diff --git a/internal/commands/inspect.go b/internal/commands/inspect.go index b1177231b..b7a93af33 100644 --- a/internal/commands/inspect.go +++ b/internal/commands/inspect.go @@ -8,14 +8,16 @@ import ( "github.com/deislabs/cnab-go/action" "github.com/deislabs/cnab-go/bundle" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" + "github.com/docker/app/internal" "github.com/docker/app/internal/cliopts" "github.com/docker/app/internal/cnab" "github.com/docker/app/internal/inspect" "github.com/docker/app/internal/packager" - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" + "github.com/docker/app/internal/store" ) const inspectExample = `- $ docker app inspect my-running-app @@ -43,7 +45,11 @@ func inspectCmd(dockerCli command.Cli, installerContext *cliopts.InstallerContex } func runInspect(dockerCli command.Cli, appName string, inspectOptions inspectOptions, installerContext *cliopts.InstallerContextOptions) error { - _, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext()) + orchestrator, err := store.GetOrchestrator(dockerCli) + if err != nil { + return err + } + _, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext(), orchestrator) if err != nil { return err } diff --git a/internal/commands/list.go b/internal/commands/list.go index ed735343b..f006060f3 100644 --- a/internal/commands/list.go +++ b/internal/commands/list.go @@ -10,10 +10,6 @@ import ( "time" "github.com/deislabs/cnab-go/action" - "github.com/docker/app/internal" - "github.com/docker/app/internal/cliopts" - "github.com/docker/app/internal/cnab" - "github.com/docker/app/internal/store" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" @@ -22,6 +18,11 @@ import ( "github.com/docker/go/canonical/json" "github.com/pkg/errors" "github.com/spf13/cobra" + + "github.com/docker/app/internal" + "github.com/docker/app/internal/cliopts" + "github.com/docker/app/internal/cnab" + "github.com/docker/app/internal/store" ) var ( @@ -71,8 +72,12 @@ func runList(dockerCli command.Cli, opts listOptions, installerContext *cliopts. if err != nil { return err } + orchestrator, err := store.GetOrchestrator(dockerCli) + if err != nil { + return err + } targetContext := dockerCli.CurrentContext() - installationStore, err := appstore.InstallationStore(targetContext) + installationStore, err := appstore.InstallationStore(targetContext, orchestrator) if err != nil { return err } diff --git a/internal/commands/remove.go b/internal/commands/remove.go index 21cd10317..a76b4b49f 100644 --- a/internal/commands/remove.go +++ b/internal/commands/remove.go @@ -34,7 +34,11 @@ func removeCmd(dockerCli command.Cli, installerContext *cliopts.InstallerContext Example: `$ docker app rm myrunningapp`, Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - _, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext()) + orchestrator, err := store.GetOrchestrator(dockerCli) + if err != nil { + return err + } + _, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext(), orchestrator) if err != nil { return err } diff --git a/internal/commands/root.go b/internal/commands/root.go index 4090b7bed..a70b23b9f 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -6,16 +6,17 @@ import ( "os" "github.com/deislabs/cnab-go/claim" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/docker/app/internal" "github.com/docker/app/internal/cliopts" "github.com/docker/app/internal/commands/build" "github.com/docker/app/internal/commands/image" "github.com/docker/app/internal/store" appstore "github.com/docker/app/internal/store" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/config" - "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type mainOptions struct { @@ -105,12 +106,12 @@ func muteDockerCli(dockerCli command.Cli) func() { } } -func prepareStores(targetContext string) (store.ImageStore, store.InstallationStore, store.CredentialStore, error) { +func prepareStores(targetContext string, orchestrator command.Orchestrator) (store.ImageStore, store.InstallationStore, store.CredentialStore, error) { appstore, err := store.NewApplicationStore(config.Dir()) if err != nil { return nil, nil, nil, err } - installationStore, err := appstore.InstallationStore(targetContext) + installationStore, err := appstore.InstallationStore(targetContext, orchestrator) if err != nil { return nil, nil, nil, err } diff --git a/internal/commands/run.go b/internal/commands/run.go index d3a268932..d8c10ccff 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -4,24 +4,22 @@ import ( "fmt" "os" - "github.com/docker/app/internal/packager" - - "github.com/docker/app/internal/image" - - "github.com/deislabs/cnab-go/driver" - "github.com/docker/app/internal/cliopts" - "github.com/deislabs/cnab-go/action" "github.com/deislabs/cnab-go/credentials" - bdl "github.com/docker/app/internal/bundle" - "github.com/docker/app/internal/cnab" - "github.com/docker/app/internal/store" + "github.com/deislabs/cnab-go/driver" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/docker/pkg/namesgenerator" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + + bdl "github.com/docker/app/internal/bundle" + "github.com/docker/app/internal/cliopts" + "github.com/docker/app/internal/cnab" + "github.com/docker/app/internal/image" + "github.com/docker/app/internal/packager" + "github.com/docker/app/internal/store" ) type runOptions struct { @@ -103,7 +101,11 @@ func runBundle(dockerCli command.Cli, bndl *image.AppImage, opts runOptions, ins if err := packager.CheckAppVersion(dockerCli.Err(), bndl.Bundle); err != nil { return err } - _, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext()) + orchestrator, err := store.GetOrchestrator(dockerCli) + if err != nil { + return err + } + _, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext(), orchestrator) if err != nil { return err } diff --git a/internal/commands/update.go b/internal/commands/update.go index dd2fbbbfb..06e2a090f 100644 --- a/internal/commands/update.go +++ b/internal/commands/update.go @@ -4,16 +4,17 @@ import ( "fmt" "os" - "github.com/deislabs/cnab-go/driver" - "github.com/deislabs/cnab-go/action" "github.com/deislabs/cnab-go/credentials" + "github.com/deislabs/cnab-go/driver" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" + "github.com/docker/app/internal/bundle" "github.com/docker/app/internal/cliopts" "github.com/docker/app/internal/cnab" "github.com/docker/app/internal/packager" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" + "github.com/docker/app/internal/store" ) type updateOptions struct { @@ -41,7 +42,11 @@ func updateCmd(dockerCli command.Cli, installerContext *cliopts.InstallerContext } func runUpdate(dockerCli command.Cli, installationName string, opts updateOptions, installerContext *cliopts.InstallerContextOptions) error { - imageStore, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext()) + orchestrator, err := store.GetOrchestrator(dockerCli) + if err != nil { + return err + } + imageStore, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext(), orchestrator) if err != nil { return err } diff --git a/internal/store/app.go b/internal/store/app.go index 32c584420..e20aa6195 100644 --- a/internal/store/app.go +++ b/internal/store/app.go @@ -5,9 +5,12 @@ import ( "os" "path/filepath" - "github.com/deislabs/cnab-go/utils/crud" + cnabCrud "github.com/deislabs/cnab-go/utils/crud" + "github.com/docker/cli/cli/command" digest "github.com/opencontainers/go-digest" "github.com/pkg/errors" + + appCrud "github.com/docker/app/internal/store/crud" ) const ( @@ -51,12 +54,28 @@ func NewApplicationStore(configDir string) (*ApplicationStore, error) { } // InstallationStore initializes and returns a context based installation store -func (a ApplicationStore) InstallationStore(context string) (InstallationStore, error) { - path := filepath.Join(a.path, InstallationStoreDirectory, makeDigestedDirectory(context)) - if err := os.MkdirAll(path, 0755); err != nil { - return nil, errors.Wrapf(err, "failed to create installation store directory for context %q", context) +func (a ApplicationStore) InstallationStore(context string, orchestrator command.Orchestrator) (InstallationStore, error) { + switch { + // FIXME What if orchestrator.HasKubernetes() and still want to use local store? + case orchestrator.HasKubernetes(): + // FIXME Get this namespace, labelKey and labelValue dynamically through cli opts + k8sStore, err := appCrud.NewKubernetesSecretsStore( + appCrud.DefaultKubernetesNamespace, + appCrud.LabelKV{ + appCrud.DefaultSecretLabelKey, + appCrud.DefaultSecretLabelValue, + }) + if err != nil { + return nil, err + } + return &installationStore{store: k8sStore}, nil + default: + path := filepath.Join(a.path, InstallationStoreDirectory, makeDigestedDirectory(context)) + if err := os.MkdirAll(path, 0755); err != nil { + return nil, errors.Wrapf(err, "failed to create installation store directory for context %q", context) + } + return &installationStore{store: cnabCrud.NewFileSystemStore(path, "json")}, nil } - return &installationStore{store: crud.NewFileSystemStore(path, "json")}, nil } // CredentialStore initializes and returns a context based credential store diff --git a/internal/store/app_test.go b/internal/store/app_test.go index 6bcc4c2a5..a9c655499 100644 --- a/internal/store/app_test.go +++ b/internal/store/app_test.go @@ -3,6 +3,7 @@ package store import ( "testing" + "github.com/docker/cli/cli/command" "gotest.tools/assert" "gotest.tools/fs" ) @@ -17,7 +18,7 @@ func TestNewApplicationStoreInitializesDirectories(t *testing.T) { assert.Equal(t, appstore.path, dockerConfigDir.Join("app")) // an installation store is created per context - _, err = appstore.InstallationStore("my-context") + _, err = appstore.InstallationStore("my-context", command.OrchestratorSwarm) assert.NilError(t, err) // a credential store is created per context diff --git a/internal/store/crud/kubernetes.go b/internal/store/crud/kubernetes.go new file mode 100644 index 000000000..6f8efb537 --- /dev/null +++ b/internal/store/crud/kubernetes.go @@ -0,0 +1,179 @@ +package crud + +import ( + "os" + "path/filepath" + "strings" + + cnabCrud "github.com/deislabs/cnab-go/utils/crud" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + DefaultSecretLabelKey = "installation_store" + DefaultSecretLabelValue = "docker-app" + + // Kubernetes namespace where to store the secrets representing the installation claims + DefaultKubernetesNamespace = "docker-app" +) + +// NewKubernetesSecretsStore creates a Store backed by kubernetes secrets. +// Each key is represented by a secret in a kubernetes namespace. +func NewKubernetesSecretsStore(namespace string, label LabelKV) (cnabCrud.Store, error) { + k8sClient, err := getClient() + if err != nil { + return nil, err + } + k8sStore := kubernetesSecretsStore{ + namespace: namespace, + client: k8sClient, + labelKV: label, + } + err = k8sStore.ensureNamespace() + if err != nil { + return nil, err + } + return k8sStore, nil +} + +type LabelKV [2]string + +func (l LabelKV) getKey() string { + return l[0] +} + +func (l LabelKV) getValue() string { + return l[1] +} + +func (l LabelKV) String() string { + return strings.Join(l[:], "=") +} + +type kubernetesSecretsStore struct { + namespace string + client corev1.CoreV1Interface + labelKV LabelKV +} + +func (s kubernetesSecretsStore) List() ([]string, error) { + secrets, err := s.client.Secrets(s.namespace).List(metav1.ListOptions{ + LabelSelector: s.labelKV.String(), + }) + if err != nil { + return nil, err + } + var secretNames []string + for _, scr := range secrets.Items { + secretNames = append(secretNames, scr.Name) + } + return secretNames, nil +} + +func (s kubernetesSecretsStore) Store(name string, data []byte) error { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + s.labelKV.getKey(): s.labelKV.getValue(), + }, + }, + Data: map[string][]byte{ + name: data, + }, + } + _, err := s.Read(name) + if err == nil { + if _, err := s.client.Secrets(s.namespace).Update(secret); err != nil { + return err + } + return nil + } + if _, err := s.client.Secrets(s.namespace).Create(secret); err != nil { + return err + } + return nil +} + +func (s kubernetesSecretsStore) Read(name string) ([]byte, error) { + secret, err := s.client.Secrets(s.namespace).Get(name, metav1.GetOptions{}) + if err != nil { + if err == errors.NewNotFound(schema.GroupResource{Resource: "secrets"}, name) { + return nil, cnabCrud.ErrRecordDoesNotExist + } + return nil, err + } + return secret.Data[name], nil +} + +func (s kubernetesSecretsStore) Delete(name string) error { + if err := s.client.Secrets(s.namespace).Delete(name, &metav1.DeleteOptions{}); err != nil { + return err + } + return nil +} + +func (s kubernetesSecretsStore) ensureNamespace() error { + namespaceService := NewNamespaceService(s.client) + if _, err := namespaceService.Get(s.namespace); err != nil { + _, err = namespaceService.Create(s.namespace) + return err + } + return nil +} + +type NamespaceService struct { + client corev1.CoreV1Interface +} + +func NewNamespaceService(clientset corev1.CoreV1Interface) *NamespaceService { + return &NamespaceService{ + client: clientset, + } +} + +func (n NamespaceService) Get(name string) (*v1.Namespace, error) { + namespace, err := n.client.Namespaces().Get(name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return namespace, nil +} + +func (n NamespaceService) Create(name string) (*v1.Namespace, error) { + namespace, err := n.client.Namespaces().Create(&v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }) + if err != nil { + return nil, err + } + return namespace, nil +} + +// FIXME Only reading from kubectl default file config for now +func getClient() (corev1.CoreV1Interface, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + kubeconfig := filepath.Join(home, ".kube", "config") + // use the current context in kubeconfig + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, err + } + // create the client + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + return clientset.CoreV1(), nil +} diff --git a/internal/store/installation.go b/internal/store/installation.go index e1bcc110d..20ebb4450 100644 --- a/internal/store/installation.go +++ b/internal/store/installation.go @@ -4,12 +4,11 @@ import ( "encoding/json" "fmt" - "github.com/docker/app/internal/image" - - "github.com/docker/cnab-to-oci/relocation" - "github.com/deislabs/cnab-go/claim" "github.com/deislabs/cnab-go/utils/crud" + "github.com/docker/cnab-to-oci/relocation" + + "github.com/docker/app/internal/image" ) // InstallationStore is an interface to persist, delete, list and read installations. @@ -74,7 +73,6 @@ func (i installationStore) Read(installationName string) (*Installation, error) if err != nil { if err == crud.ErrRecordDoesNotExist { return nil, fmt.Errorf("Installation %q not found", installationName) - } return nil, err } diff --git a/internal/store/installation_test.go b/internal/store/installation_test.go index 6ff75cbb7..2c6990c2d 100644 --- a/internal/store/installation_test.go +++ b/internal/store/installation_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/deislabs/cnab-go/claim" + "github.com/docker/cli/cli/command" "gotest.tools/assert" "gotest.tools/fs" ) @@ -15,7 +16,7 @@ func TestStoreAndReadInstallation(t *testing.T) { defer dockerConfigDir.Remove() appstore, err := NewApplicationStore(dockerConfigDir.Path()) assert.NilError(t, err) - installationStore, err := appstore.InstallationStore("my-context") + installationStore, err := appstore.InstallationStore("my-context", command.OrchestratorSwarm) assert.NilError(t, err) expectedInstallation := &Installation{ diff --git a/internal/store/utils.go b/internal/store/utils.go new file mode 100644 index 000000000..b405c0065 --- /dev/null +++ b/internal/store/utils.go @@ -0,0 +1,14 @@ +package store + +import ( + "os" + + "github.com/docker/cli/cli/command" + + "github.com/docker/app/internal" +) + +func GetOrchestrator(cli command.Cli) (command.Orchestrator, error) { + orchestratorRaw := os.Getenv(internal.DockerStackOrchestratorEnvVar) + return cli.StackOrchestrator(orchestratorRaw) +}