forked from toolshed/abra
WIP: foundations for app deploy
This commit is contained in:
133
client/stack/loader.go
Normal file
133
client/stack/loader.go
Normal file
@ -0,0 +1,133 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/schema"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
||||
func LoadComposefile(opts options.Deploy) (*composetypes.Config, error) {
|
||||
configDetails, err := getConfigDetails(opts.Composefiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
config, err := loader.Load(configDetails)
|
||||
if err != nil {
|
||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||
return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s",
|
||||
propertyWarnings(fpe.Properties))
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
logrus.Warnf("Ignoring unsupported options: %s\n\n",
|
||||
strings.Join(unsupportedProperties, ", "))
|
||||
}
|
||||
|
||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||
if len(deprecatedProperties) > 0 {
|
||||
logrus.Warnf("Ignoring deprecated options:\n\n%s\n\n",
|
||||
propertyWarnings(deprecatedProperties))
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
||||
dicts := []map[string]interface{}{}
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
dicts = append(dicts, configFile.Config)
|
||||
}
|
||||
|
||||
return dicts
|
||||
}
|
||||
|
||||
func propertyWarnings(properties map[string]string) string {
|
||||
var msgs []string
|
||||
for name, description := range properties {
|
||||
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
|
||||
}
|
||||
sort.Strings(msgs)
|
||||
return strings.Join(msgs, "\n\n")
|
||||
}
|
||||
|
||||
func getConfigDetails(composefiles []string) (composetypes.ConfigDetails, error) {
|
||||
var details composetypes.ConfigDetails
|
||||
|
||||
absPath, err := filepath.Abs(composefiles[0])
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.WorkingDir = filepath.Dir(absPath)
|
||||
|
||||
details.ConfigFiles, err = loadConfigFiles(composefiles)
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
// Take the first file version (2 files can't have different version)
|
||||
details.Version = schema.Version(details.ConfigFiles[0].Config)
|
||||
details.Environment, err = buildEnvironment(os.Environ())
|
||||
return details, err
|
||||
}
|
||||
|
||||
func buildEnvironment(env []string) (map[string]string, error) {
|
||||
result := make(map[string]string, len(env))
|
||||
for _, s := range env {
|
||||
// if value is empty, s is like "K=", not "K".
|
||||
if !strings.Contains(s, "=") {
|
||||
return result, fmt.Errorf("unexpected environment %q", s)
|
||||
}
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
result[kv[0]] = kv[1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadConfigFiles(filenames []string) ([]composetypes.ConfigFile, error) {
|
||||
var configFiles []composetypes.ConfigFile
|
||||
|
||||
for _, filename := range filenames {
|
||||
configFile, err := loadConfigFile(filename)
|
||||
if err != nil {
|
||||
return configFiles, err
|
||||
}
|
||||
configFiles = append(configFiles, *configFile)
|
||||
}
|
||||
|
||||
return configFiles, nil
|
||||
}
|
||||
|
||||
func loadConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
||||
var bytes []byte
|
||||
var err error
|
||||
|
||||
bytes, err = ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := loader.ParseYAML(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &composetypes.ConfigFile{
|
||||
Filename: filename,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
104
client/stack/stack.go
Normal file
104
client/stack/stack.go
Normal file
@ -0,0 +1,104 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
abraClient "coopcloud.tech/abra/client"
|
||||
"coopcloud.tech/abra/client/convert"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type StackStatus struct {
|
||||
Services []swarm.Service
|
||||
Err error
|
||||
}
|
||||
|
||||
func getStackFilter(namespace string) filters.Args {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", convert.LabelNamespace+"="+namespace)
|
||||
return filter
|
||||
}
|
||||
|
||||
func getStackServiceFilter(namespace string) filters.Args {
|
||||
return getStackFilter(namespace)
|
||||
}
|
||||
|
||||
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
|
||||
filter := opt.Value()
|
||||
filter.Add("label", convert.LabelNamespace+"="+namespace)
|
||||
return filter
|
||||
}
|
||||
|
||||
func getAllStacksFilter() filters.Args {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", convert.LabelNamespace)
|
||||
return filter
|
||||
}
|
||||
|
||||
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {
|
||||
return apiclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
|
||||
}
|
||||
|
||||
func GetAllDeployedServices(contextName string) StackStatus {
|
||||
cl, err := abraClient.NewClientWithContext(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
ctx := context.Background()
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
return StackStatus{services, nil}
|
||||
}
|
||||
|
||||
func checkDaemonIsSwarmManager(contextName string) error {
|
||||
cl, err := abraClient.NewClientWithContext(contextName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := cl.Info(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Swarm.ControlAvailable {
|
||||
return errors.New("this server is not a swarm manager. Did you run \"abra server init\"?")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateStackName checks if the provided string is a valid stack name (namespace).
|
||||
// It currently only does a rudimentary check if the string is empty, or consists
|
||||
// of only whitespace and quoting characters.
|
||||
func validateStackName(namespace string) error {
|
||||
v := strings.TrimFunc(namespace, quotesOrWhitespace)
|
||||
if v == "" {
|
||||
return fmt.Errorf("invalid stack name: %q", namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func quotesOrWhitespace(r rune) bool {
|
||||
return unicode.IsSpace(r) || r == '"' || r == '\''
|
||||
}
|
||||
|
||||
func DeployStack(namespace string) {
|
||||
}
|
||||
|
||||
// TODO: Prune services from stack
|
||||
|
||||
func pruneServices() error {
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user