diff --git a/cmd/add/plugin.go b/cmd/add/plugin.go index 5e19f85155..e316aaf911 100644 --- a/cmd/add/plugin.go +++ b/cmd/add/plugin.go @@ -1,6 +1,7 @@ package add import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "github.com/devspace-cloud/devspace/pkg/util/factory" "github.com/spf13/cobra" ) @@ -32,18 +33,24 @@ devspace add plugin https://github.com/my-plugin/plugin return pluginCmd } - // Run executes the command logic func (cmd *pluginCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []string) error { f.GetLog().StartWait("Installing plugin " + args[0]) defer f.GetLog().StopWait() - err := f.NewPluginManager(f.GetLog()).Add(args[0], cmd.Version) + addedPlugin, err := f.NewPluginManager(f.GetLog()).Add(args[0], cmd.Version) if err != nil { return err } f.GetLog().StopWait() f.GetLog().Donef("Successfully installed plugin %s", args[0]) + + // Execute plugin hook + err = plugin.ExecutePluginHook([]plugin.Metadata{*addedPlugin}, cobraCmd, args, "after_install", "", "", nil) + if err != nil { + return err + } + return nil -} \ No newline at end of file +} diff --git a/cmd/analyze.go b/cmd/analyze.go index 4373b69839..cdad85131c 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -89,7 +89,7 @@ func (cmd *AnalyzeCmd) RunAnalyze(f factory.Factory, plugins []plugin.Metadata, } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "analyze", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "analyze", client.CurrentContext(), client.Namespace(), nil) if err != nil { return err } diff --git a/cmd/attach.go b/cmd/attach.go index 43c1fb7db1..091aada678 100644 --- a/cmd/attach.go +++ b/cmd/attach.go @@ -92,7 +92,7 @@ func (cmd *AttachCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "attach", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "attach", client.CurrentContext(), client.Namespace(), nil) if err != nil { return err } diff --git a/cmd/build.go b/cmd/build.go index ba6d6365a5..519ed2905f 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -94,7 +94,7 @@ func (cmd *BuildCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "build", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "build", cmd.KubeContext, cmd.Namespace, config) if err != nil { return err } diff --git a/cmd/deploy.go b/cmd/deploy.go index 16bfe85301..e083830c6a 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -33,6 +33,7 @@ type DeployCmd struct { SkipBuild bool BuildSequential bool ForceDeploy bool + SkipDeploy bool Deployments string ForceDependencies bool VerboseDependencies bool @@ -90,6 +91,7 @@ devspace deploy --kube-context=deploy-context deployCmd.Flags().BoolVar(&cmd.BuildSequential, "build-sequential", false, "Builds the images one after another instead of in parallel") deployCmd.Flags().BoolVarP(&cmd.ForceDeploy, "force-deploy", "d", false, "Forces to (re-)deploy every deployment") deployCmd.Flags().BoolVar(&cmd.ForceDependencies, "force-dependencies", true, "Forces to re-evaluate dependencies (use with --force-build --force-deploy to actually force building & deployment of dependencies)") + deployCmd.Flags().BoolVar(&cmd.SkipDeploy, "skip-deploy", false, "Skips deploying and only builds images") deployCmd.Flags().StringVar(&cmd.Deployments, "deployments", "", "Only deploy a specifc deployment (You can specify multiple deployments comma-separated") deployCmd.Flags().StringSliceVar(&cmd.Dependency, "dependency", []string{}, "Deploys only the specific named dependencies") @@ -105,7 +107,7 @@ devspace deploy --kube-context=deploy-context // Run executes the down command logic func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { - // Set config root + // set config root cmd.log = f.GetLog() configOptions := cmd.ToConfigOptions() configLoader := f.NewConfigLoader(cmd.ToConfigOptions(), cmd.log) @@ -117,43 +119,43 @@ func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd return errors.New(message.ConfigNotFound) } - // Start file logging + // start file logging logpkg.StartFileLogging() - // Validate flags + // validate flags err = cmd.validateFlags() if err != nil { return err } - // Load generated config + // load generated config generatedConfig, err := configLoader.Generated() if err != nil { return errors.Errorf("Error loading generated.yaml: %v", err) } - // Use last context if specified + // use last context if specified err = cmd.UseLastContext(generatedConfig, cmd.log) if err != nil { return err } - // Create kubectl client + // create kubectl client client, err := f.NewKubeClientFromContext(cmd.KubeContext, cmd.Namespace, cmd.SwitchContext) if err != nil { return errors.Errorf("Unable to create new kubectl client: %v", err) } - // Warn the user if we deployed into a different context before + // warn the user if we deployed into a different context before err = client.PrintWarning(generatedConfig, cmd.NoWarn, true, cmd.log) if err != nil { return err } - // Clear the dependencies & deployments cache if necessary + // clear the dependencies & deployments cache if necessary clearCache(generatedConfig, client) - // Deprecated: Fill DEVSPACE_DOMAIN vars + // deprecated: Fill DEVSPACE_DOMAIN vars err = fillDevSpaceDomainVars(client, generatedConfig) if err != nil { return err @@ -169,7 +171,7 @@ func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd generatedConfig.Vars = vars } - // Add current kube context to context + // add current kube context to context config, err := configLoader.Load() if err != nil { return err @@ -183,43 +185,44 @@ func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd } } - // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "deploy", cmd.KubeContext, cmd.Namespace) + // execute plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "deploy", client.CurrentContext(), client.Namespace(), config) if err != nil { return err } - // Create namespace if necessary + // create namespace if necessary err = client.EnsureDeployNamespaces(config, cmd.log) if err != nil { return errors.Errorf("Unable to create namespace: %v", err) } - // Create docker client + // create docker client dockerClient, err := f.NewDockerClient(cmd.log) if err != nil { dockerClient = nil } - // Create pull secrets and private registry if necessary + // create pull secrets if necessary err = f.NewPullSecretClient(config, client, dockerClient, cmd.log).CreatePullSecrets() if err != nil { cmd.log.Warn(err) } - // Create Dependencymanager + // create dependency manager manager, err := f.NewDependencyManager(config, generatedConfig, client, cmd.AllowCyclicDependencies, configOptions, cmd.log) if err != nil { return errors.Wrap(err, "new manager") } - // Dependencies + // deploy dependencies err = manager.DeployAll(dependency.DeployOptions{ Dependencies: cmd.Dependency, SkipPush: cmd.SkipPush, ForceDeployDependencies: cmd.ForceDependencies, SkipBuild: cmd.SkipBuild, ForceBuild: cmd.ForceBuild, + SkipDeploy: cmd.SkipDeploy, ForceDeploy: cmd.ForceDeploy, Verbose: cmd.VerboseDependencies, }) @@ -227,9 +230,9 @@ func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd return errors.Wrap(err, "deploy dependencies") } - // Only deploy if we don't want to deploy a dependency specificly + // only deploy if we don't want to deploy a dependency specificly if len(cmd.Dependency) == 0 { - // Build images + // build images builtImages := make(map[string]string) if cmd.SkipBuild == false { builtImages, err = f.NewBuildController(config, generatedConfig.GetActive(), client).Build(&build.Options{ @@ -245,7 +248,7 @@ func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd return err } - // Save config if an image was built + // save cache if an image was built if len(builtImages) > 0 { err := configLoader.SaveGenerated() if err != nil { @@ -254,33 +257,35 @@ func (cmd *DeployCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd } } - // What deployments should be deployed + // what deployments should be deployed deployments := []string{} - if cmd.Deployments != "" { - deployments = strings.Split(cmd.Deployments, ",") - for index := range deployments { - deployments[index] = strings.TrimSpace(deployments[index]) + if cmd.SkipDeploy == false { + if cmd.Deployments != "" { + deployments = strings.Split(cmd.Deployments, ",") + for index := range deployments { + deployments[index] = strings.TrimSpace(deployments[index]) + } } - } - // Deploy all defined deployments - err = f.NewDeployController(config, generatedConfig.GetActive(), client).Deploy(&deploy.Options{ - ForceDeploy: cmd.ForceDeploy, - BuiltImages: builtImages, - Deployments: deployments, - }, cmd.log) - if err != nil { - return err + // deploy all defined deployments + err = f.NewDeployController(config, generatedConfig.GetActive(), client).Deploy(&deploy.Options{ + ForceDeploy: cmd.ForceDeploy, + BuiltImages: builtImages, + Deployments: deployments, + }, cmd.log) + if err != nil { + return err + } } } - // Update last used kube context & save generated yaml + // update last used kube context & save generated yaml err = updateLastKubeContext(configLoader, client, generatedConfig) if err != nil { return errors.Wrap(err, "update last kube context") } - // Wait if necessary + // wait if necessary if cmd.Wait { report, err := f.NewAnalyzer(client, f.GetLog()).CreateReport(client.Namespace(), analyze.Options{Wait: true, Patient: true, Timeout: cmd.Timeout}) if err != nil { diff --git a/cmd/dev.go b/cmd/dev.go index e7225e7e26..13f7d4a207 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -25,7 +25,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/loader" latest "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/util/exit" "github.com/devspace-cloud/devspace/pkg/util/factory" "github.com/devspace-cloud/devspace/pkg/util/log" @@ -147,6 +147,10 @@ Open terminal instead of logs: // Run executes the command logic func (cmd *DevCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { + if cmd.Interactive { + cmd.log.Warn("Interactive mode flag is deprecated and will be removed in the future. Please take a look at https://devspace.sh/cli/docs/guides/interactive-mode on how to transition to an interactive profile") + } + // Set config root cmd.log = f.GetLog() cmd.configLoader = f.NewConfigLoader(cmd.ToConfigOptions(), cmd.log) @@ -225,7 +229,7 @@ func (cmd *DevCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *c } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "dev", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "dev", client.CurrentContext(), client.Namespace(), config) if err != nil { return err } @@ -242,7 +246,7 @@ func (cmd *DevCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *c dockerClient = nil } - registryClient := registry.NewClient(config, client, dockerClient, cmd.log) + registryClient := pullsecrets.NewClient(config, client, dockerClient, cmd.log) err = registryClient.CreatePullSecrets() if err != nil { cmd.log.Warn(err) diff --git a/cmd/enter.go b/cmd/enter.go index 8f1e17a631..ec7e156002 100644 --- a/cmd/enter.go +++ b/cmd/enter.go @@ -97,7 +97,7 @@ func (cmd *EnterCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "enter", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "enter", client.CurrentContext(), client.Namespace(), nil) if err != nil { return err } diff --git a/cmd/init.go b/cmd/init.go index c125331d14..14cedcc2e4 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "io/ioutil" "os" "path/filepath" @@ -46,6 +47,9 @@ const ( // The default name for the production profile productionProfileName = "production" + + // The default name for the interactive profile + interactiveProfileName = "interactive" ) // InitCmd is a struct that defines a command call for "init" @@ -61,7 +65,7 @@ type InitCmd struct { } // NewInitCmd creates a new init command -func NewInitCmd(f factory.Factory) *cobra.Command { +func NewInitCmd(f factory.Factory, plugins []plugin.Metadata) *cobra.Command { cmd := &InitCmd{ log: f.GetLog(), } @@ -79,7 +83,7 @@ folder. Creates a devspace.yaml with all configuration. `, Args: cobra.NoArgs, RunE: func(cobraCmd *cobra.Command, args []string) error { - return cmd.Run(f, cobraCmd, args) + return cmd.Run(f, plugins, cobraCmd, args) }, } @@ -92,7 +96,7 @@ folder. Creates a devspace.yaml with all configuration. } // Run executes the command logic -func (cmd *InitCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []string) error { +func (cmd *InitCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { // Check if config already exists cmd.log = f.GetLog() configLoader := f.NewConfigLoader(nil, cmd.log) @@ -115,10 +119,16 @@ func (cmd *InitCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []strin // Delete config & overwrite config os.Remove(constants.DefaultVarsPath) + // Execute plugin hook + err := plugin.ExecutePluginHook(plugins, cobraCmd, args, "init", "", "", nil) + if err != nil { + return err + } + // Create config config := configLoader.New() - //Create ConfigureManager + // Create ConfigureManager configureManager := f.NewConfigureManager(config, cmd.log) // Print DevSpace logo @@ -452,6 +462,28 @@ func (cmd *InitCmd) addProfileConfig(config *latest.Config) error { Patches: patches, }) } + if ok { + config.Profiles = append(config.Profiles, &latest.ProfileConfig{ + Name: interactiveProfileName, + Patches: []*latest.PatchConfig{ + { + Operation: "add", + Path: "dev.interactive", + Value: map[string]bool{ + "defaultEnabled": true, + }, + }, + { + Operation: "add", + Path: "images." + defaultImageName + ".entrypoint", + Value: []string{ + "sleep", + "9999999999", + }, + }, + }, + }) + } } return nil } diff --git a/cmd/logs.go b/cmd/logs.go index 256d3a715a..0c0636af8f 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -100,7 +100,7 @@ func (cmd *LogsCmd) RunLogs(f factory.Factory, plugins []plugin.Metadata, cobraC } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "logs", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "logs", client.CurrentContext(), client.Namespace(), nil) if err != nil { return err } diff --git a/cmd/open.go b/cmd/open.go index cd118b4a9a..2e48df8e9a 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -127,7 +127,7 @@ func (cmd *OpenCmd) RunOpen(f factory.Factory, plugins []plugin.Metadata, cobraC } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "open", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "open", client.CurrentContext(), client.Namespace(), nil) if err != nil { return err } diff --git a/cmd/print.go b/cmd/print.go index d17399bf5c..896a7a6bf3 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "path/filepath" "github.com/devspace-cloud/devspace/cmd/flags" @@ -22,7 +23,7 @@ type PrintCmd struct { } // NewPrintCmd creates a new devspace print command -func NewPrintCmd(f factory.Factory, globalFlags *flags.GlobalFlags) *cobra.Command { +func NewPrintCmd(f factory.Factory, globalFlags *flags.GlobalFlags, plugins []plugin.Metadata) *cobra.Command { cmd := &PrintCmd{GlobalFlags: globalFlags} printCmd := &cobra.Command{ @@ -36,7 +37,7 @@ Prints the configuration for the current or given profile after all patching and variable substitution #######################################################`, RunE: func(cobraCmd *cobra.Command, args []string) error { - return cmd.Run(f, cobraCmd, args) + return cmd.Run(f, plugins, cobraCmd, args) }, } @@ -46,7 +47,7 @@ profile after all patching and variable substitution } // Run executes the command logic -func (cmd *PrintCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []string) error { +func (cmd *PrintCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { // Set config root log := f.GetLog() configOptions := cmd.ToConfigOptions() @@ -64,6 +65,12 @@ func (cmd *PrintCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []stri return err } + // Execute plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "print", "", "", loadedConfig) + if err != nil { + return err + } + bsConfig, err := yaml.Marshal(loadedConfig) if err != nil { return err diff --git a/cmd/purge.go b/cmd/purge.go index 97868e2c5d..2881beee2f 100644 --- a/cmd/purge.go +++ b/cmd/purge.go @@ -104,7 +104,7 @@ func (cmd *PurgeCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd } // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "purge", cmd.KubeContext, cmd.Namespace) + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "purge", client.CurrentContext(), client.Namespace(), nil) if err != nil { return err } diff --git a/cmd/remove/plugin.go b/cmd/remove/plugin.go index f1456ff59a..d5a7c01958 100644 --- a/cmd/remove/plugin.go +++ b/cmd/remove/plugin.go @@ -1,12 +1,12 @@ package remove import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "github.com/devspace-cloud/devspace/pkg/util/factory" "github.com/spf13/cobra" ) type pluginCmd struct { - } func newPluginCmd(f factory.Factory) *cobra.Command { @@ -33,10 +33,22 @@ devspace remove plugin my-plugin // Run executes the command logic func (cmd *pluginCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []string) error { + pluginManager := f.NewPluginManager(f.GetLog()) + _, oldPlugin, err := pluginManager.GetByName(args[0]) + if err != nil { + return err + } else if oldPlugin != nil { + // Execute plugin hook + err = plugin.ExecutePluginHook([]plugin.Metadata{*oldPlugin}, cobraCmd, args, "before_remove", "", "", nil) + if err != nil { + return err + } + } + f.GetLog().StartWait("Removing plugin " + args[0]) defer f.GetLog().StopWait() - err := f.NewPluginManager(f.GetLog()).Remove(args[0]) + err = pluginManager.Remove(args[0]) if err != nil { return err } @@ -44,4 +56,4 @@ func (cmd *pluginCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []str f.GetLog().StopWait() f.GetLog().Donef("Successfully removed plugin %s", args[0]) return nil -} \ No newline at end of file +} diff --git a/cmd/render.go b/cmd/render.go index 05c480430b..186dbe1248 100644 --- a/cmd/render.go +++ b/cmd/render.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "os" "strings" @@ -40,7 +41,7 @@ type RenderCmd struct { } // NewRenderCmd creates a new devspace render command -func NewRenderCmd(f factory.Factory, globalFlags *flags.GlobalFlags) *cobra.Command { +func NewRenderCmd(f factory.Factory, globalFlags *flags.GlobalFlags, plugins []plugin.Metadata) *cobra.Command { cmd := &RenderCmd{GlobalFlags: globalFlags} renderCmd := &cobra.Command{ @@ -55,7 +56,7 @@ be deployed via helm and kubectl, but skips actual deployment. #######################################################`, RunE: func(cobraCmd *cobra.Command, args []string) error { - return cmd.Run(f, cobraCmd, args) + return cmd.Run(f, plugins, cobraCmd, args) }, } @@ -77,7 +78,7 @@ deployment. } // Run executes the command logic -func (cmd *RenderCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []string) error { +func (cmd *RenderCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { // Set config root log := f.GetLog() if cmd.ShowLogs == false { @@ -126,6 +127,12 @@ func (cmd *RenderCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []str return errors.Errorf("Unable to create new kubectl client: %v", err) } + // Execute plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "render", client.CurrentContext(), client.Namespace(), config) + if err != nil { + return err + } + // Create Dependencymanager if cmd.SkipDependencies == false { manager, err := f.NewDependencyManager(config, generatedConfig, client, cmd.AllowCyclicDependencies, configOptions, log) diff --git a/cmd/restart.go b/cmd/restart.go index d5fc2b6fee..233dfa0f57 100644 --- a/cmd/restart.go +++ b/cmd/restart.go @@ -88,14 +88,14 @@ func (cmd *RestartCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCm return err } - // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "restart", cmd.KubeContext, cmd.Namespace) + // Get config with adjusted cluster config + config, err := configLoader.Load() if err != nil { return err } - // Get config with adjusted cluster config - config, err := configLoader.Load() + // Execute plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "restart", client.CurrentContext(), client.Namespace(), config) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 5bc605c2d3..67146791e0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,7 +17,6 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/loader" "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "github.com/devspace-cloud/devspace/pkg/devspace/upgrade" - "github.com/devspace-cloud/devspace/pkg/util/analytics/cloudanalytics" "github.com/devspace-cloud/devspace/pkg/util/exit" "github.com/devspace-cloud/devspace/pkg/util/factory" flagspkg "github.com/devspace-cloud/devspace/pkg/util/flags" @@ -32,16 +31,16 @@ import ( ) // NewRootCmd returns a new root command -func NewRootCmd(f factory.Factory) *cobra.Command { +func NewRootCmd(f factory.Factory, plugins []plugin.Metadata) *cobra.Command { return &cobra.Command{ Use: "devspace", SilenceUsage: true, SilenceErrors: true, Short: "Welcome to the DevSpace!", - PersistentPreRun: func(cobraCmd *cobra.Command, args []string) { + PersistentPreRunE: func(cobraCmd *cobra.Command, args []string) error { // don't do anything if it is a plugin command if cobraCmd.Annotations != nil && cobraCmd.Annotations[plugin.PluginCommandAnnotation] == "true" { - return + return nil } log := f.GetLog() @@ -64,6 +63,14 @@ func NewRootCmd(f factory.Factory) *cobra.Command { log.Infof("Applying extra flags from environment: %s", strings.Join(extraFlags, " ")) } } + + // call root plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "root", "", "", nil) + if err != nil { + return err + } + + return nil }, Long: `DevSpace accelerates developing, deploying and debugging applications with Docker and Kubernetes. Get started by running the init command in one of your projects: @@ -79,9 +86,6 @@ func Execute() { // disable klog disableKlog() - // report any panics - defer cloudanalytics.ReportPanics() - // create a new factory f := factory.DefaultFactory() @@ -93,7 +97,6 @@ func Execute() { // execute command err := rootCmd.Execute() - cloudanalytics.SendCommandEvent(err) if err != nil { // Check if return code error retCode, ok := errors.Cause(err).(*exit.ReturnCodeError) @@ -111,16 +114,17 @@ func Execute() { // BuildRoot creates a new root command from the func BuildRoot(f factory.Factory) *cobra.Command { - rootCmd := NewRootCmd(f) - persistentFlags := rootCmd.PersistentFlags() - globalFlags = flags.SetGlobalFlags(persistentFlags) - // list plugins plugins, err := f.NewPluginManager(f.GetLog()).List() if err != nil { f.GetLog().Fatal(err) } + // build the root cmd + rootCmd := NewRootCmd(f, plugins) + persistentFlags := rootCmd.PersistentFlags() + globalFlags = flags.SetGlobalFlags(persistentFlags) + // Add sub commands rootCmd.AddCommand(add.NewAddCmd(f, globalFlags, plugins)) rootCmd.AddCommand(cleanup.NewCleanupCmd(f, globalFlags, plugins)) @@ -135,23 +139,23 @@ func BuildRoot(f factory.Factory) *cobra.Command { rootCmd.AddCommand(restore.NewRestoreCmd(f, globalFlags, plugins)) // Add main commands - rootCmd.AddCommand(NewInitCmd(f)) + rootCmd.AddCommand(NewInitCmd(f, plugins)) rootCmd.AddCommand(NewRestartCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewDevCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewBuildCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewSyncCmd(f, globalFlags, plugins)) - rootCmd.AddCommand(NewRenderCmd(f, globalFlags)) + rootCmd.AddCommand(NewRenderCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewPurgeCmd(f, globalFlags, plugins)) - rootCmd.AddCommand(NewUpgradeCmd()) + rootCmd.AddCommand(NewUpgradeCmd(plugins)) rootCmd.AddCommand(NewDeployCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewEnterCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewAnalyzeCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewLogsCmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewOpenCmd(f, globalFlags, plugins)) - rootCmd.AddCommand(NewUICmd(f, globalFlags)) + rootCmd.AddCommand(NewUICmd(f, globalFlags, plugins)) rootCmd.AddCommand(NewRunCmd(f, globalFlags)) rootCmd.AddCommand(NewAttachCmd(f, globalFlags, plugins)) - rootCmd.AddCommand(NewPrintCmd(f, globalFlags)) + rootCmd.AddCommand(NewPrintCmd(f, globalFlags, plugins)) // Add plugin commands plugin.AddPluginCommands(rootCmd, plugins, "") diff --git a/cmd/set/analytics.go b/cmd/set/analytics.go deleted file mode 100644 index 0a4872d120..0000000000 --- a/cmd/set/analytics.go +++ /dev/null @@ -1,56 +0,0 @@ -package set - -import ( - "github.com/devspace-cloud/devspace/pkg/util/analytics" - "github.com/devspace-cloud/devspace/pkg/util/factory" - - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type analyticsCmd struct{} - -func newAnalyticsCmd(f factory.Factory) *cobra.Command { - cmd := &analyticsCmd{} - - return &cobra.Command{ - Use: "analytics", - Short: "Update analytics settings", - Long: ` -####################################################### -############### devspace set analytics ################ -####################################################### -Example: -devspace set analytics disabled true -####################################################### - `, - Args: cobra.RangeArgs(1, 2), - RunE: func(cobraCmd *cobra.Command, args []string) error { - return cmd.RunAnalyticsConfig(f, cobraCmd, args) - }, - } -} - -// RunAnalyticsConfig executes the "devspace set analytics" logic -func (*analyticsCmd) RunAnalyticsConfig(f factory.Factory, cobraCmd *cobra.Command, args []string) error { - log := f.GetLog() - analytics, err := analytics.GetAnalytics() - if err != nil { - return errors.Wrap(err, "get analytics config") - } - - if args[0] == "disabled" { - if len(args) == 2 && (args[1] == "false" || args[1] == "0") { - err = analytics.Enable() - } else { - err = analytics.Disable() - } - } - - if err != nil { - return errors.Wrap(err, "set analytics config") - } - - log.Infof("Successfully updated analytics config") - return nil -} diff --git a/cmd/set/set.go b/cmd/set/set.go index 1bc6892110..a060466414 100644 --- a/cmd/set/set.go +++ b/cmd/set/set.go @@ -19,7 +19,6 @@ func NewSetCmd(f factory.Factory, plugins []plugin.Metadata) *cobra.Command { Args: cobra.NoArgs, } - setCmd.AddCommand(newAnalyticsCmd(f)) setCmd.AddCommand(newVarCmd(f)) // Add plugin commands diff --git a/cmd/sync.go b/cmd/sync.go index f9d552806f..e25dd7dc81 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -130,12 +130,6 @@ func (cmd *SyncCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd * return err } - // Execute plugin hook - err = plugin.ExecutePluginHook(plugins, "sync", cmd.KubeContext, cmd.Namespace) - if err != nil { - return err - } - var config *latest.Config if configLoader.Exists() { config, err = configLoader.Load() @@ -144,6 +138,12 @@ func (cmd *SyncCmd) Run(f factory.Factory, plugins []plugin.Metadata, cobraCmd * } } + // Execute plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "sync", client.CurrentContext(), client.Namespace(), config) + if err != nil { + return err + } + // Build params params := targetselector.CmdParameter{ ContainerName: cmd.Container, diff --git a/cmd/ui.go b/cmd/ui.go index 19e0ebcebd..f6d5a9da38 100644 --- a/cmd/ui.go +++ b/cmd/ui.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "io/ioutil" "net/http" "time" @@ -33,7 +34,7 @@ type UICmd struct { } // NewUICmd creates a new ui command -func NewUICmd(f factory.Factory, globalFlags *flags.GlobalFlags) *cobra.Command { +func NewUICmd(f factory.Factory, globalFlags *flags.GlobalFlags, plugins []plugin.Metadata) *cobra.Command { cmd := &UICmd{ GlobalFlags: globalFlags, log: log.GetInstance(), @@ -51,7 +52,7 @@ Opens the localhost UI in the browser `, Args: cobra.NoArgs, RunE: func(cobraCmd *cobra.Command, args []string) error { - return cmd.RunUI(f, cobraCmd, args) + return cmd.RunUI(f, plugins, cobraCmd, args) }, } @@ -63,7 +64,7 @@ Opens the localhost UI in the browser } // RunUI executes the functionality "devspace ui" -func (cmd *UICmd) RunUI(f factory.Factory, cobraCmd *cobra.Command, args []string) error { +func (cmd *UICmd) RunUI(f factory.Factory, plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { // Set config root cmd.log = f.GetLog() configLoader := f.NewConfigLoader(cmd.ToConfigOptions(), cmd.log) @@ -181,6 +182,12 @@ func (cmd *UICmd) RunUI(f factory.Factory, cobraCmd *cobra.Command, args []strin // Override error runtime handler log.OverrideRuntimeErrorHandler(true) + // Execute plugin hook + err = plugin.ExecutePluginHook(plugins, cobraCmd, args, "ui", client.CurrentContext(), client.Namespace(), config) + if err != nil { + return err + } + // Check if we should force the port var forcePort *int if cmd.Port != 0 { diff --git a/cmd/update/plugin.go b/cmd/update/plugin.go index 7ba58779b5..74edbc7f8e 100644 --- a/cmd/update/plugin.go +++ b/cmd/update/plugin.go @@ -1,6 +1,7 @@ package update import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "github.com/devspace-cloud/devspace/pkg/util/factory" "github.com/spf13/cobra" ) @@ -34,15 +35,34 @@ devspace update plugin my-plugin // Run executes the command logic func (cmd *pluginCmd) Run(f factory.Factory, cobraCmd *cobra.Command, args []string) error { + pluginManager := f.NewPluginManager(f.GetLog()) + _, oldPlugin, err := pluginManager.GetByName(args[0]) + if err != nil { + return err + } else if oldPlugin != nil { + // Execute plugin hook + err = plugin.ExecutePluginHook([]plugin.Metadata{*oldPlugin}, cobraCmd, args, "before_update", "", "", nil) + if err != nil { + return err + } + } + f.GetLog().StartWait("Updating plugin " + args[0]) defer f.GetLog().StopWait() - err := f.NewPluginManager(f.GetLog()).Update(args[0], cmd.Version) + updatedPlugin, err := pluginManager.Update(args[0], cmd.Version) if err != nil { return err } f.GetLog().StopWait() f.GetLog().Donef("Successfully updated plugin %s", args[0]) + + // Execute plugin hook + err = plugin.ExecutePluginHook([]plugin.Metadata{*updatedPlugin}, cobraCmd, args, "after_update", "", "", nil) + if err != nil { + return err + } + return nil -} \ No newline at end of file +} diff --git a/cmd/upgrade.go b/cmd/upgrade.go index e2a81af6ad..5450c756af 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/devspace-cloud/devspace/pkg/devspace/plugin" "github.com/devspace-cloud/devspace/pkg/devspace/upgrade" "github.com/pkg/errors" @@ -11,7 +12,7 @@ import ( type UpgradeCmd struct{} // NewUpgradeCmd creates a new upgrade command -func NewUpgradeCmd() *cobra.Command { +func NewUpgradeCmd(plugins []plugin.Metadata) *cobra.Command { cmd := &UpgradeCmd{} upgradeCmd := &cobra.Command{ @@ -24,15 +25,23 @@ func NewUpgradeCmd() *cobra.Command { Upgrades the DevSpace CLI to the newest version #######################################################`, Args: cobra.NoArgs, - RunE: cmd.Run, + RunE: func(cobraCmd *cobra.Command, args []string) error { + return cmd.Run(plugins, cobraCmd, args) + }, } return upgradeCmd } // Run executes the command logic -func (cmd *UpgradeCmd) Run(cobraCmd *cobra.Command, args []string) error { - err := upgrade.Upgrade() +func (cmd *UpgradeCmd) Run(plugins []plugin.Metadata, cobraCmd *cobra.Command, args []string) error { + // Execute plugin hook + err := plugin.ExecutePluginHook(plugins, cobraCmd, args, "upgrade", "", "", nil) + if err != nil { + return err + } + + err = upgrade.Upgrade() if err != nil { return errors.Errorf("Couldn't upgrade: %v", err) } diff --git a/docs/pages/configuration/hooks/basics.mdx b/docs/pages/configuration/hooks/basics.mdx index ef70128e39..5bc0f99bf6 100644 --- a/docs/pages/configuration/hooks/basics.mdx +++ b/docs/pages/configuration/hooks/basics.mdx @@ -21,6 +21,10 @@ For a complete example take a look at [this example project on GitHub](https://g ::: This tells DevSpace to execute the command `echo before image building` before any image will be built. You are able to define hooks for the following life cycle events: +- **before pull secret creation**: Will be executed before creating any image pull secrets. Value: `when.before.pullSecrets: all` +- **after pull secret creation**: Will be executed after image pull secrets have been successfully created. Value: `when.after.pullSecrets: all` +- **before dependency deployment**: Will be executed before deploying any dependencies. Value: `when.before.dependencies: all` +- **after dependency deployment**: Will be executed after dependencies have been successfully deployed. Value: `when.after.dependencies: all` - **before image building**: Will be executed before building any images. Value: `when.before.images: all` - **after image building**: Will be executed after images have been successfully built. Value: `when.after.images: all` - **before deploying**: Will be executed before any deployment is deployed. Value: `when.before.deployments: all` @@ -31,3 +35,26 @@ This tells DevSpace to execute the command `echo before image building` before a :::info Errors in Hooks If any hook returns a non zero exit code, DevSpace will abort and print an error message. ::: + +## Execute hooks only on a certain operating system + +Hooks can be executed only on certain operating systems: + +```yaml {5,12} +hooks: +- command: echo + args: + - before image building on windows + os: windows + when: + before: + images: all +- command: echo + args: + - before image building on mac and linux + os: darwin,linux + when: + before: + images: all +``` + diff --git a/docs/pages/configuration/profiles/basics.mdx b/docs/pages/configuration/profiles/basics.mdx index 0e05d7fb42..8ce7cb7d0f 100644 --- a/docs/pages/configuration/profiles/basics.mdx +++ b/docs/pages/configuration/profiles/basics.mdx @@ -5,14 +5,22 @@ sidebar_label: Basics import FragmentInfoPrintConfig from '../../fragments/tip-print-config.mdx'; -DevSpace allows you to define different profiles for different use cases (e.g. working on different services in the same project, starting certain debugging environments) or for different deployment targets (e.g. dev, staging production). +DevSpace allows you to define different profiles for different use cases (e.g. working on different services in the same project, starting certain debugging environments) or for different deployment targets (e.g. dev, staging production). Profiles allow you to define: -- [`replace`](../../configuration/profiles/replace.mdx) statements to override entire sections of the config -- [`patches`](../../configuration/profiles/patches.mdx) to modify certain parts of the config. - -:::note Combine Replace & Patches -It is possible to use the `replace` and `patches` together within one profile. +- [`replace`](../../configuration/profiles/replace.mdx) statements to override entire sections of the config. +- [`merge`](../../configuration/profiles/merge.mdx) patches ([JSON Merge Patch, RFC 7386](https://tools.ietf.org/html/rfc7386)) to change specific parts of the config. +- [`strategicMerge`](../../configuration/profiles/strategic-merge.mdx) patches (similar to [kubernetes strategic patches](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#use-strategic-merge-patch-to-update-a-deployment-using-the-retainkeys-strategy)) to change specific parts of the config. +- [`patches`](../../configuration/profiles/patches.mdx) ([JSON Patch, RFC 6902](https://tools.ietf.org/html/rfc6902)) to modify certain paths in the config. + +:::note Combine several strategies +It is possible to use several strategies together within one profile. The execution order inside a single profile is: +1. `replace` +2. `merge` +3. `strategicMerge` +4. `patches` + +If you want to change the execution order or need multiple patches take a look at [profile parents](../../configuration/profiles/parents.mdx) ::: :::tip Debug Profiles diff --git a/docs/pages/configuration/profiles/merge.mdx b/docs/pages/configuration/profiles/merge.mdx new file mode 100644 index 0000000000..ac8c7420cb --- /dev/null +++ b/docs/pages/configuration/profiles/merge.mdx @@ -0,0 +1,65 @@ +--- +title: "Profiles: Merge" +sidebar_label: merge +--- + +Merge patches are a way to perform specific overrides to the configuration without having to create a completely separate config file. Patch functionality follows [JSON Merge Patch, RFC 7386](https://tools.ietf.org/html/rfc7386) semantics. + +### Example + +Merge patches are ideal for reflecting changes between different environments, e.g. dev, staging and production. +```yaml {16-30} +images: + backend: + image: john/devbackend + backend-debugger: + image: john/debugger +deployments: +- name: backend + helm: + componentChart: true + values: + containers: + - image: john/devbackend + - image: john/debugger +profiles: +- name: production + merge: + images: + # Change the backend image + backend: + image: john/prodbackend + # Delete the backend-debugger image + backend-debugger: null + # Override deployments + deployments: + - name: backend + helm: + componentChart: true + values: + containers: + - image: john/prodbackend +``` +**Explanation:** +- The above example defines 1 profile: `production` +- When using the profile `production`, the config is merged with the given merge patch at `profiles[0].merge`. +- Merge patches follow the rules as defined in [JSON Merge Patch, RFC 7386](https://tools.ietf.org/html/rfc7386): + - Arrays are overriden + - Objects are merged together + - Keys that have a `null` value are removed from objects +- The resulting config used in-memory when the profile `production` is used would look like this (you can check via `devspace print -p production`): + +```yaml +# In-Memory Config After Applying Merge Patches For Profile `production` +images: + backend: + image: john/prodbackend +deployments: +- name: backend + helm: + componentChart: true + values: + containers: + - image: john/prodbackend +``` + diff --git a/docs/pages/fragments/profile-parent.mdx b/docs/pages/configuration/profiles/parents.mdx similarity index 97% rename from docs/pages/fragments/profile-parent.mdx rename to docs/pages/configuration/profiles/parents.mdx index 5ff4da20a0..301bc1d176 100644 --- a/docs/pages/fragments/profile-parent.mdx +++ b/docs/pages/configuration/profiles/parents.mdx @@ -1,3 +1,8 @@ +--- +title: "Profiles: Parents" +sidebar_label: parents +--- + The `parents` option is optional and expects the names of other profiles which should be applied before this profile. The profiles are applied in the order they are specified. It is also possible to apply profiles from distant `devspace.yaml`s. The kind of profile inheritance that the `parents` option provides can help to reduce redundancy when multiple profiles need to change the config in a similar way. @@ -7,8 +12,7 @@ A parent profile is applied before the profile that defines the parent. A parent ::: #### Example: Defining a Parent Profile -```yaml {16} -version: v1beta9 +```yaml {16-17} images: backend: image: john/devbackend diff --git a/docs/pages/configuration/profiles/patches.mdx b/docs/pages/configuration/profiles/patches.mdx index d21c70859c..96f3da6758 100644 --- a/docs/pages/configuration/profiles/patches.mdx +++ b/docs/pages/configuration/profiles/patches.mdx @@ -4,7 +4,6 @@ sidebar_label: patches --- import FragmentInfoPrintConfig from '../../fragments/tip-print-config.mdx'; -import FragmentProfileParent from '../../fragments/profile-parent.mdx'; Patches are a way to perform minor overrides to the configuration without having to create a separate config file. Patch functionality follows JSON Patch([RFC](https://tools.ietf.org/html/rfc6902)) semantics, as implemented by the [yaml-patch](https://github.com/krishicks/yaml-patch) library. @@ -137,12 +136,6 @@ profiles: - Users can permanently switch to the `staging` profile using: `devspace use profile staging` - Users can permanently switch to the `production` profile using: `devspace use profile production` - -### `parents` - - - - ## Useful Commands ### `devspace print -p [profile]` diff --git a/docs/pages/configuration/profiles/replace.mdx b/docs/pages/configuration/profiles/replace.mdx index 7f225fcc54..21c4a5d99d 100644 --- a/docs/pages/configuration/profiles/replace.mdx +++ b/docs/pages/configuration/profiles/replace.mdx @@ -4,7 +4,6 @@ sidebar_label: replace --- import FragmentInfoPrintConfig from '../../fragments/tip-print-config.mdx'; -import FragmentProfileParent from '../../fragments/profile-parent.mdx'; ## Configuration @@ -68,12 +67,6 @@ deployments: As shown in this example, it is possible to use `replace` and `patch` options in combination. ::: - -### `parents` - - - - ## Useful Commands ### `devspace print -p [profile]` diff --git a/docs/pages/configuration/profiles/strategic-merge.mdx b/docs/pages/configuration/profiles/strategic-merge.mdx new file mode 100644 index 0000000000..91c5d1e287 --- /dev/null +++ b/docs/pages/configuration/profiles/strategic-merge.mdx @@ -0,0 +1,82 @@ +--- +title: "Profiles: Strategic Merge" +sidebar_label: strategicMerge +--- + +Strategic Merge patches are a way to perform specific overrides to the configuration without having to create a completely separate config file. +They are very similar to [merge patches](../../configuration/profiles/merge.mdx) and [kubernetes strategic patches](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/#use-strategic-merge-patch-to-update-a-deployment-using-the-retainkeys-strategy) with the distinct difference that special keys will be merged differently as in a standard merge patch. +Especially changes to a single `deployment` are easier to realize with a strategic merge patch. + +### Example + +Strategic Merge patches are ideal for reflecting changes between different environments, e.g. dev, staging and production. +```yaml {25-38} +images: + backend: + image: john/devbackend + backend-debugger: + image: john/debugger +deployments: +- name: backend + helm: + componentChart: true + values: + service: + ports: + - port: 3000 + containers: + - image: john/devbackend + - image: john/debugger +- name: untouched-deployment + helm: + componentChart: true + values: + containers: + - image: untouched/deployment +profiles: +- name: production + strategicMerge: + images: + # Change the backend image + backend: + image: john/prodbackend + # Delete the backend-debugger image + backend-debugger: null + # Override backend deployment and leave others + deployments: + - name: backend + helm: + values: + containers: + - image: john/prodbackend +``` +**Explanation:** +- The above example defines 1 profile: `production` +- When using the profile `production`, the config is merged with the given strategic merge patch at `profiles[0].strategicMerge`. +- Strategic Merge patches follow the same rules as [merge patches](../../configuration/profiles/merge.mdx), but are merged for special keys differently: + - Array items will mostly be merged instead of replaced (depends on the key). For example, if you define an `deployments` array in a strategic merge patch, the array items will be matched by `name` and added to the array instead of replacing the complete `deployments` section. +- The resulting config used in-memory when the profile `production` is used would look like this (you can check via `devspace print -p production`): + +```yaml +# In-Memory Config After Applying Merge Patches For Profile `production` +images: + backend: + image: john/prodbackend +deployments: +- name: backend + helm: + componentChart: true + values: + containers: + - image: john/prodbackend + service: + ports: + - port: 3000 +- name: untouched-deployment + helm: + componentChart: true + values: + containers: + - image: untouched/deployment +``` + diff --git a/docs/pages/configuration/pullSecrets/basics.mdx b/docs/pages/configuration/pullSecrets/basics.mdx new file mode 100644 index 0000000000..d32bdc785b --- /dev/null +++ b/docs/pages/configuration/pullSecrets/basics.mdx @@ -0,0 +1,78 @@ +--- +title: Pull Secrets +sidebar_label: pullSecrets +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +DevSpace allows you to configure additional [pull secrets](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/) that should be created in the target namespace. For each configured pull secret in the `devspace.yaml`, DevSpace will create a secret in the target namespace and add the secret to the `imagePullSecrets` field of the service account. + +For [images](../images/basics), DevSpace will also automatically create a pull secret, if authentication data can be found in the local docker store and creation is [enabled](../images/pull-secrets) via `createPullSecret`. + +Image Pull Secrets are defined in the `pullSecrets` section of the `devspace.yaml`. + + + + +```yaml +# If you don't want to specify the password and username directly in the config +# you can use variables, .env file or save the credentials in the local docker store +pullSecrets: +- registry: my-registry.com:5000 + username: my-user + password: my-password +``` + + + + +```yaml +# If you leave out the username & password DevSpace will try +# to get these from the local docker store. Make sure you +# are logged into the registry with `docker login my-registry.com:5000` +pullSecrets: +- registry: my-registry.com:5000 +``` + + + + +```yaml +pullSecrets: +- registry: my-registry.com:5000 + secret: my-pull-secret-name + serviceAccounts: + - default + - my-other-service-account +``` + + + + +## Configuration + +### `registry` +The `registry` option is mandatory and expects a string with the registry name for which a pull secret should be created for. + +### `username` +The `username` option is optional and expects a string with the username to login into the registry. If this field is empty, DevSpace will try to find out username and password from the local docker store. + +### `password` +The `password` option is optional and expects a string with the password to login into the registry. If this field is empty, DevSpace will try to find out username and password from the local docker store. + +### `email` +The `email` option is optional and expects a string with the email to login into the registry. This can be left empty usually since username and password are enough to log into a docker registry. If empty, the default value will be `noreply@devspace.cloud`. + +### `secret` +The `secret` option is optional and expects a string how the secret should be named. If empty, DevSpace will automatically create a meaningful name for the secret. + +### `serviceAccounts` +The `serviceAccounts` option is optional and expects an array of strings to which the pull secret should be added. If an image pull secret is added to a service account, the service account is able to pull images from that registry even without specifying the image pull secret in a pod definition. If this is empty, DevSpace will add the pull secret to the `default` serviceaccount. \ No newline at end of file diff --git a/docs/pages/configuration/variables/basics.mdx b/docs/pages/configuration/variables/basics.mdx index 9820bfcba8..89c498b302 100644 --- a/docs/pages/configuration/variables/basics.mdx +++ b/docs/pages/configuration/variables/basics.mdx @@ -98,6 +98,13 @@ The `source` option of a config variable expects either: - [`input`](../../configuration/variables/source-input.mdx) means to ask the user a question **once** (values will be cached in `.devspace/generated.yaml`) ::: +:::warning Pass Variables via CLI +A useful feature in DevSpace is that you can also specify variables as flags, which will override previous values (if there are any): +``` +devspace deploy --var VAR1=VALUE1 --var VAR2=VALUE2 +``` +::: + :::note Input Caching If `source` is either `all` or `input` and the variable is not defined, the user will be asked to provide a value either using a generic question or the one provided via the [`question` option](../../configuration/variables/source-input.mdx#question). The user-provided value will be cached in `.devspace/generated.yaml` and the user will only be asked again after the cache has been cleared first using: ```bash @@ -105,8 +112,6 @@ devspace reset vars ``` ::: - - ## Predefined Variables DevSpace provides some variables that are filled automatically and can be used within the config. These can be helpful for image tagging and other use cases: diff --git a/docs/pages/guides/interactive-mode.mdx b/docs/pages/guides/interactive-mode.mdx index 1a06190c23..6d7fd1cb57 100644 --- a/docs/pages/guides/interactive-mode.mdx +++ b/docs/pages/guides/interactive-mode.mdx @@ -3,6 +3,28 @@ title: Interactive Mode sidebar_label: Interactive Mode --- +:::warning Deprecation of interactive mode +Using `devspace dev -i` is deprecated, and the flag will be removed in the future! Make sure to change your `devspace.yaml`'s to have an `interactive` mode profile instead: + +```yaml +... +profiles: +- name: interactive + patches: + - op: add + path: dev.interactive + value: + defaultEnabled: true + - op: add + path: dev.MY_IMAGE.entrypoint + value: + - sleep + - 99999999 +``` + +This profile can be activated via `-p interactive` and has the same functionality as using the flag `-i`. +::: + The development mode of DevSpace can be started using the `-i / --interactive` flag which overrides the `ENTRYPOINT` of an image with `[sleep, 999999]` and opens an interactive terminal session for one of the containers that use the 'sleeping' image. Due to the `ENTRYPOINT` override, the application has not been started within the container and the user can start the application manually through the interactive terminal session. ## Start Interactive Mode diff --git a/docs/pages/guides/plugins.mdx b/docs/pages/guides/plugins.mdx new file mode 100644 index 0000000000..475bdd66cc --- /dev/null +++ b/docs/pages/guides/plugins.mdx @@ -0,0 +1,181 @@ +--- +title: DevSpace Plugins +sidebar_label: Plugins +--- + +The functionality of DevSpace can be extended and changed via plugins. Plugins are managed through DevSpace and are contained in a single binary or shell script. Plugins are able to extend DevSpace in the following ways: +- Add new commands to DevSpace (e.g. `devspace login` or `devspace list users`) +- Add new [predefined variables](../configuration/variables/basics) +- Execute hooks at specific events (like a command execution) + +## Installing a plugin + +Plugins can be installed from an URL, Git Repository or local file. To install a plugin run: +``` +# Add a plugin from a git repository +devspace add plugin https://github.com/my-organization/my-repo + +# Add a plugin from an URL +devspace add plugin https://myorg.com/path/to/plugin.yaml + +# Add a plugin from a local path +devspace add plugin ./plugin.yaml +``` + +After installing a plugin you can check all your existing plugins via: +``` +devspace list plugins +``` + +## Updating a plugin + +To update a DevSpace plugin run the following command: + +``` +# Enter the name and not the path +devspace update plugin PLUGIN_NAME + +# This only works for GIT plugins +devspace update plugin PLUGIN_NAME --version GIT_TAG +``` + +## Removing a plugin + +To remove a plugin via the DevSpace command line: +``` +# Enter the name and not the path +devspace remove plugin PLUGIN_NAME +``` + +If the plugin cannot be removed because a certain hook fails (or any other reason), you can also delete the plugin manually. DevSpace saves all plugin data in `$HOME/.devspace/plugins`. You will have to check each folders `plugin.yaml` to see which plugin is stored in the folder. To remove a plugin, simply delete the complete plugin folder. + +## Developing a custom DevSpace plugin + +Creating an own DevSpace plugin is quite easy. You only need a `plugin.yaml` that specifies where DevSpace can find the plugin binary and how DevSpace should execute it. You can also take a look at the [devspace-plugin-example](https://github.com/devspace-cloud/devspace-cloud-plugin) project for a complete example. +For each installed plugin, DevSpace will create a folder in `$HOME/.devspace/plugins` with a `plugin.yaml` and a downloaded or copied `binary` that will be executed. + +### plugin.yaml + +The `plugin.yaml` specifies how the plugin is installed and integrates into DevSpace and consists of the following parts. + +#### `name` + +Name of the plugin as shown in devspace list plugins and used for devspace update plugin and devspace remove plugin. (e.g. `my-devspace-plugin`) + +#### `version` + +The semantic current version of the plugin (e.g. `0.0.1`) + +#### `binaries` + +This section states where DevSpace can retrieve the plugin binary for the current operating system and architecture. If devspace cannot find a binary for the current [runtime.GOOS](https://golang.org/pkg/runtime/#pkg-constants) and [runtime.GOARCH](https://golang.org/pkg/runtime/#pkg-constants) it will not install the plugin. +The `binaries` section expects an array with objects that can have the following properties: +* `os` is the runtime.GOOS name of the operating system (e.g. darwin, windows, linux etc.) +* `arch` is the runtime.GOARCH name of the system (e.g. amd64, 386 etc.) +* `path` is the URL to the binary to download or the local path to the binary to copy + +#### `commands` + +This section specifies which commands should be added to DevSpace. It expects an array with objects that can have the following properties: +* `name` of the command that should be added to devspace (e.g. `login` will add `devspace login`) +* `baseArgs` these args are prepended to the plugin binary, so when a user will call 'devspace login other --flag 123', devspace will call the plugin binary with 'plugin-binary baseArgs... other --flag 123' +* `usage` the usage of the command to print in `devspace --help` +* `description` the description of the command to print in `devspace --help` +* `subCommand` (Optional) the subcommand to append the command to (e.g. `add` will add the command to `devspace add`) + +DevSpace will forward all passed arguments and flags to the plugin command. + +#### `vars` + +This section specifies which [predefined variables](../configuration/variables/basics) are added to DevSpace. These variable values will be retrieved from the plugin binary instead of asking the user. It expects an array with objects that can have the following properties: +* `name` of the predefined variable to add (e.g. `EXAMPLE_VARIABLE` which can then be used in a `devspace.yaml` as `${EXAMPLE_VARIABLE}`) +* `baseArgs` these args are appended to the plugin binary (e.g. `["print", "var", "test"]` will cause devspace to call the plugin binary with: `plugin-binary print var test`) + +DevSpace expects the plugin binary to either fail (exit code unequal zero) or print the variable value to the stdout stream. Furthermore when executing the plugin-binary, DevSpace will set the following environment variables: +- `DEVSPACE_PLUGIN_OS_ARGS` all arguments that were used to call the current command encoded as JSON (e.g. `["devspace", "dev", "--wait", "--force-build"]`) +- `DEVSPACE_PLUGIN_KUBE_NAMESPACE_FLAG` the value of `--namespace` if set (e.g. `namespace`) +- `DEVSPACE_PLUGIN_KUBE_CONTEXT_FLAG` the value of `--kube-context` if set (e.g. `my-kube-context`) + +#### `hooks` + +This section specifies certain plugin commands that should be executed at certain DevSpace events. It expects an array with objects that can have the following properties: +* `event` name of the event when to execute the command. The following events exist: + * `after_install` executed after the plugin was installed + * `before_update` executed before the plugin will be updated + * `after_update` executed after the plugin was updated + * `before_remove` executed before the plugin will be removed + * `root` executed at the beginning of a devspace command execution + * `analyze`, `attach`, `build`, `deploy`, `dev`, `enter`, `init`, `logs`, `open`, `print`, `purge`, `render`, `restart`, `run`, `sync`, `ui`, `upgrade` are executed after the corresponding devspace command has loaded the config and created a kubernetes client (if there is a config to load or a kubernetes client to create) +* `baseArgs` these args are appended to the plugin binary (e.g. `["run", "my", "command"]` will cause devspace to call the plugin binary with: `plugin-binary run my command`) +* `background` if true will execute the hook in the background and continue DevSpace command execution + +If a non-background hook fails (exit code unequals zero) DevSpace will stop command execution and the complete DevSpace command fails. Furthermore when executing the plugin-binary, DevSpace will set the following environment variables (if they apply for the event): +- `DEVSPACE_PLUGIN_OS_ARGS` all arguments that were used to call the current command encoded as JSON (e.g. `["devspace", "dev", "--wait", "--force-build"]`) +- `DEVSPACE_PLUGIN_CONFIG` the config that was loaded for the command as yaml encoded (all profiles and variables are resolved at this point) +- `DEVSPACE_PLUGIN_COMMAND` the name of the DevSpace command that was executed (e.g. `dev`) +- `DEVSPACE_PLUGIN_COMMAND_LINE` the complete name of the DevSpace command that was executed (e.g. `devspace dev [FLAGS]`) +- `DEVSPACE_PLUGIN_COMMAND_FLAGS` the flags that were passed to the DevSpace command encoded as JSON (e.g. `["--namespace", "test", "--skip-build", "true"]`) +- `DEVSPACE_PLUGIN_COMMAND_ARGS` the arguments that were passed to the DevSpace command encoded as JSON (without any flags) (e.g. `["arg1"]`) +- `DEVSPACE_PLUGIN_KUBE_NAMESPACE_FLAG` the kubernetes namespace where DevSpace will operate in (e.g. `namespace`) +- `DEVSPACE_PLUGIN_KUBE_CONTEXT_FLAG` the kubernetes context where DevSpace will operate in (e.g. `my-kube-context`) + +### Example + +An example `plugin.yaml` could look like this: +```yaml +name: devspace-plugin-example +version: 0.0.1 +commands: + # This will add the command devspace login + - name: "login" + # these args are prepended to the plugin binary, so when a user will call 'devspace login test test2 --flag 123' + # devspace will call the plugin binary with 'plugin-binary other command test test2 --flag 123' + baseArgs: ["other", "command"] + usage: "short description of command" + description: "long description of command" + # You can also add commands under already existing devspace subcommands + # This will add the devspace command: devspace list env + - name: "env" + baseArgs: ["list", "env"] + subCommand: "list" +# Hooks are called before certain already existing commands are executed +# in devspace, for example devspace dev +hooks: + # will be executed when devspace print is run by the user + - event: print + # this will call the plugin binary before devspace print is called with: 'plugin-binary list env' + baseArgs: ["list", "env"] + # root is executed before any other event and command execution except for other plugin commands + - event: root + baseArgs: ["login"] +# You can also add predefined variables for the config via plugins +vars: + # the name of the predefined variable + # that can be used within any devspace.yaml + - name: EXAMPLE_USER + # this will call the plugin binary when resolving this variable and expects the variable + # output on stdout. + baseArgs: ["print", "env", "USER"] + - name: EXAMPLE_HOME + baseArgs: [ "print", "env", "HOME" ] +# In this section the plugin binaries (or scripts) and their locations are defined +# if devspace cannot find a binary for the current runtime.GOOS and runtime.GOARCH +# it will not install the plugin +binaries: + - os: darwin + arch: amd64 + # can be either an URL or local path + path: ./main + - os: linux + arch: amd64 + path: main + - os: linux + arch: "386" + path: main + - os: windows + arch: amd64 + path: path/to/main.exe + - os: windows + arch: "386" + path: https://my-domain.url/path/to/windows.exe +``` \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index 360fc098c8..19481365bd 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -99,10 +99,14 @@ module.exports = { label: 'profiles', items: [ 'configuration/profiles/basics', - 'configuration/profiles/patches', 'configuration/profiles/replace', + 'configuration/profiles/merge', + 'configuration/profiles/strategic-merge', + 'configuration/profiles/patches', + 'configuration/profiles/parents', ], }, + 'configuration/pullSecrets/basics', 'configuration/commands/basics', 'configuration/hooks/basics', 'configuration/env-file', @@ -120,6 +124,7 @@ module.exports = { 'guides/ci-cd-integration', 'guides/dev-staging-production', 'guides/image-building', + 'guides/plugins', 'guides/remote-debugging', 'guides/community-projects', ], diff --git a/examples/quickstart/devspace.yaml b/examples/quickstart/devspace.yaml index b1740a781d..78987bce48 100755 --- a/examples/quickstart/devspace.yaml +++ b/examples/quickstart/devspace.yaml @@ -24,4 +24,4 @@ dev: sync: - imageName: default excludePaths: - - node_modules + - node_modules \ No newline at end of file diff --git a/go.mod b/go.mod index f878bde704..2ff6663f97 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/docker/go-connections v0.4.0 github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 + github.com/evanphx/json-patch/v5 v5.1.0 github.com/ghodss/yaml v1.0.0 github.com/golang/protobuf v1.4.2 github.com/google/go-github v17.0.0+incompatible // indirect @@ -45,7 +46,7 @@ require ( github.com/rhysd/go-github-selfupdate v0.0.0-20180520142321-41c1bbb0804a github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 - github.com/shirou/gopsutil v0.0.0-20190627142359-4c8b404ee5c5 + github.com/shirou/gopsutil v0.0.0-20190627142359-4c8b404ee5c5 // indirect github.com/sirupsen/logrus v1.6.0 github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c github.com/spf13/cobra v1.0.0 diff --git a/go.sum b/go.sum index b782cffd90..d50e93b412 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg= +github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= diff --git a/hack/build-all.bash b/hack/build-all.bash index 5b39d8bc5d..b5d31e0e76 100755 --- a/hack/build-all.bash +++ b/hack/build-all.bash @@ -25,7 +25,7 @@ if [[ "$(pwd)" != "${DEVSPACE_ROOT}" ]]; then fi GO_BUILD_CMD="go build -a" -GO_BUILD_LDFLAGS="-s -w -X main.commitHash=${COMMIT_HASH} -X main.buildDate=${DATE} -X main.version=${VERSION} -X github.com/devspace-cloud/devspace/pkg/util/analytics.token=${ANALYTICS_TOKEN} -X github.com/devspace-cloud/devspace/pkg/util/analytics.eventEndpoint=${ANALYTICS_ENDPOINT_EVENT} -X github.com/devspace-cloud/devspace/pkg/util/analytics.userEndpoint=${ANALYTICS_ENDPOINT_USER}" +GO_BUILD_LDFLAGS="-s -w -X main.commitHash=${COMMIT_HASH} -X main.buildDate=${DATE} -X main.version=${VERSION}" if [[ -z "${DEVSPACE_BUILD_PLATFORMS}" ]]; then DEVSPACE_BUILD_PLATFORMS="linux windows darwin" diff --git a/pkg/devspace/build/builder/docker/docker.go b/pkg/devspace/build/builder/docker/docker.go index ca4fee6b89..19777c3b1e 100644 --- a/pkg/devspace/build/builder/docker/docker.go +++ b/pkg/devspace/build/builder/docker/docker.go @@ -16,7 +16,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" dockerclient "github.com/devspace-cloud/devspace/pkg/devspace/docker" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" logpkg "github.com/devspace-cloud/devspace/pkg/util/log" "github.com/docker/distribution/reference" @@ -80,7 +80,7 @@ func (b *Builder) BuildImage(contextPath, dockerfilePath string, entrypoint []st ) // Display nice registry name - registryURL, err := registry.GetRegistryFromImageName(b.helper.ImageName) + registryURL, err := pullsecrets.GetRegistryFromImageName(b.helper.ImageName) if err != nil { return err } @@ -290,7 +290,7 @@ func (b *Builder) BuildImage(contextPath, dockerfilePath string, entrypoint []st // Authenticate authenticates the client with a remote registry func (b *Builder) Authenticate() (*types.AuthConfig, error) { - registryURL, err := registry.GetRegistryFromImageName(b.helper.ImageName + ":" + b.helper.ImageTag) + registryURL, err := pullsecrets.GetRegistryFromImageName(b.helper.ImageName + ":" + b.helper.ImageTag) if err != nil { return nil, err } diff --git a/pkg/devspace/build/builder/kaniko/build_pod.go b/pkg/devspace/build/builder/kaniko/build_pod.go index 2de98eeb17..a1c6da14d8 100644 --- a/pkg/devspace/build/builder/kaniko/build_pod.go +++ b/pkg/devspace/build/builder/kaniko/build_pod.go @@ -11,7 +11,7 @@ import ( "fmt" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -47,12 +47,12 @@ var defaultResources = &availableResources{ func (b *Builder) getBuildPod(buildID string, options *types.ImageBuildOptions, dockerfilePath string) (*k8sv1.Pod, error) { kanikoOptions := b.helper.ImageConf.Build.Kaniko - registryURL, err := registry.GetRegistryFromImageName(b.FullImageName) + registryURL, err := pullsecrets.GetRegistryFromImageName(b.FullImageName) if err != nil { return nil, err } - pullSecretName := registry.GetRegistryAuthSecretName(registryURL) + pullSecretName := pullsecrets.GetRegistryAuthSecretName(registryURL) if b.PullSecretName != "" { pullSecretName = b.PullSecretName } diff --git a/pkg/devspace/build/builder/kaniko/kaniko.go b/pkg/devspace/build/builder/kaniko/kaniko.go index 2e8f64fae0..e060d8610b 100644 --- a/pkg/devspace/build/builder/kaniko/kaniko.go +++ b/pkg/devspace/build/builder/kaniko/kaniko.go @@ -15,7 +15,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/devspace/docker" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/devspace/services" "github.com/devspace-cloud/devspace/pkg/devspace/services/targetselector" logpkg "github.com/devspace-cloud/devspace/pkg/util/log" @@ -110,7 +110,7 @@ func (b *Builder) createPullSecret(log logpkg.Logger) error { return nil } - registryURL, err := registry.GetRegistryFromImageName(b.FullImageName) + registryURL, err := pullsecrets.GetRegistryFromImageName(b.FullImageName) if err != nil { return err } @@ -130,9 +130,9 @@ func (b *Builder) createPullSecret(log logpkg.Logger) error { password = authConfig.IdentityToken } - registryClient := registry.NewClient(nil, b.helper.KubeClient, b.dockerClient, log) + registryClient := pullsecrets.NewClient(nil, b.helper.KubeClient, b.dockerClient, log) - return registryClient.CreatePullSecret(®istry.PullSecretOptions{ + return registryClient.CreatePullSecret(&pullsecrets.PullSecretOptions{ Namespace: b.BuildNamespace, RegistryURL: registryURL, Username: username, diff --git a/pkg/devspace/config/loader/merge.go b/pkg/devspace/config/loader/merge.go new file mode 100644 index 0000000000..cb1f0c0d9b --- /dev/null +++ b/pkg/devspace/config/loader/merge.go @@ -0,0 +1,217 @@ +package loader + +import ( + "encoding/json" + "fmt" + "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" + jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "reflect" +) + +// ApplyStrategicMerge applies the strategic merge patches +func ApplyStrategicMerge(config map[interface{}]interface{}, profile map[interface{}]interface{}) (map[interface{}]interface{}, error) { + if profile == nil || profile["strategicMerge"] == nil { + return config, nil + } + + mergeMap, ok := profile["strategicMerge"].(map[interface{}]interface{}) + if !ok { + return nil, errors.Errorf("profiles.%v.strategicMerge is not an object", profile["name"]) + } + + mergeBytes, err := json.Marshal(convertFrom(mergeMap)) + if err != nil { + return nil, errors.Wrap(err, "marshal merge") + } + + originalBytes, err := json.Marshal(convertFrom(config)) + if err != nil { + return nil, errors.Wrap(err, "marshal merge") + } + + schema, err := strategicpatch.NewPatchMetaFromStruct(&latest.Config{}) + if err != nil { + return nil, err + } + + out, err := strategicpatch.StrategicMergePatchUsingLookupPatchMeta(originalBytes, mergeBytes, PatchMetaFromStruct{PatchMetaFromStruct: schema}) + if err != nil { + return nil, errors.Wrap(err, "create strategic merge patch") + } + + strMap := map[string]interface{}{} + err = json.Unmarshal(out, &strMap) + if err != nil { + return nil, err + } + + return convertBack(strMap).(map[interface{}]interface{}), nil +} + +// ApplyMerge applies the merge patches +func ApplyMerge(config map[interface{}]interface{}, profile map[interface{}]interface{}) (map[interface{}]interface{}, error) { + if profile == nil || profile["merge"] == nil { + return config, nil + } + + mergeMap, ok := profile["merge"].(map[interface{}]interface{}) + if !ok { + return nil, errors.Errorf("profiles.%v.merge is not an object", profile["name"]) + } + + mergeBytes, err := json.Marshal(convertFrom(mergeMap)) + if err != nil { + return nil, errors.Wrap(err, "marshal merge") + } + + originalBytes, err := json.Marshal(convertFrom(config)) + if err != nil { + return nil, errors.Wrap(err, "marshal merge") + } + + out, err := jsonpatch.MergePatch(originalBytes, mergeBytes) + if err != nil { + return nil, errors.Wrap(err, "create merge patch") + } + + strMap := map[string]interface{}{} + err = json.Unmarshal(out, &strMap) + if err != nil { + return nil, err + } + + return convertBack(strMap).(map[interface{}]interface{}), nil +} + +func convertBack(v interface{}) interface{} { + switch x := v.(type) { + case map[string]interface{}: + m := map[interface{}]interface{}{} + for k, v2 := range x { + m[k] = convertBack(v2) + } + v = m + + case []interface{}: + for i, v2 := range x { + x[i] = convertBack(v2) + } + + case map[interface{}]interface{}: + for k, v2 := range x { + x[k] = convertBack(v2) + } + } + + return v +} + +func convertFrom(v interface{}) interface{} { + switch x := v.(type) { + case map[interface{}]interface{}: + m := map[string]interface{}{} + for k, v2 := range x { + switch k2 := k.(type) { + case string: // Fast check if it's already a string + m[k2] = convertFrom(v2) + default: + m[fmt.Sprint(k)] = convertFrom(v2) + } + } + v = m + + case []interface{}: + for i, v2 := range x { + x[i] = convertFrom(v2) + } + + case map[string]interface{}: + for k, v2 := range x { + x[k] = convertFrom(v2) + } + } + + return v +} + +type PatchMetaFromStruct struct { + strategicpatch.PatchMetaFromStruct +} + +func LookupPatchMetadataForMap(t reflect.Type) ( + elemType reflect.Type, patchStrategies []string, patchMergeKey string, e error) { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Map && t.Kind() != reflect.Interface { + e = fmt.Errorf("merging an object in json but data type is not map, instead is: %s", + t.Kind().String()) + return + } + if t.Kind() == reflect.Interface { + return t, []string{}, "", nil + } + + return t.Elem(), []string{}, "", nil +} + +// we have to override map handling since otherwise it would produce errors +func (s PatchMetaFromStruct) LookupPatchMetadataForSlice(key string) (strategicpatch.LookupPatchMeta, strategicpatch.PatchMeta, error) { + subschema, patchMeta, err := s.LookupPatchMetadataForStruct(key) + if err != nil { + return nil, strategicpatch.PatchMeta{}, err + } + elemPatchMetaFromStruct := subschema.(PatchMetaFromStruct) + t := elemPatchMetaFromStruct.T + + var elemType reflect.Type + switch t.Kind() { + // If t is an array or a slice, get the element type. + // If element is still an array or a slice, return an error. + // Otherwise, return element type. + case reflect.Array, reflect.Slice: + elemType = t.Elem() + if elemType.Kind() == reflect.Array || elemType.Kind() == reflect.Slice { + return nil, strategicpatch.PatchMeta{}, errors.New("unexpected slice of slice") + } + // If t is an pointer, get the underlying element. + // If the underlying element is neither an array nor a slice, the pointer is pointing to a slice, + // e.g. https://github.com/kubernetes/kubernetes/blob/bc22e206c79282487ea0bf5696d5ccec7e839a76/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch_test.go#L2782-L2822 + // If the underlying element is either an array or a slice, return its element type. + case reflect.Ptr: + t = t.Elem() + if t.Kind() == reflect.Array || t.Kind() == reflect.Slice || t.Kind() == reflect.Map { + t = t.Elem() + } + elemType = t + case reflect.Map: + elemType = t.Elem() + case reflect.Interface: + elemType = t + default: + return nil, strategicpatch.PatchMeta{}, fmt.Errorf("expected slice or array type, but got: %s", t.Kind().String()) + } + + return PatchMetaFromStruct{strategicpatch.PatchMetaFromStruct{T: elemType}}, patchMeta, nil +} + +// we have to override map handling since otherwise it would produce errors +func (s PatchMetaFromStruct) LookupPatchMetadataForStruct(key string) (strategicpatch.LookupPatchMeta, strategicpatch.PatchMeta, error) { + fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := LookupPatchMetadataForMap(s.PatchMetaFromStruct.T) + if err != nil { + l, p, err := s.PatchMetaFromStruct.LookupPatchMetadataForStruct(key) + if err != nil { + return nil, strategicpatch.PatchMeta{}, err + } + + return PatchMetaFromStruct{l.(strategicpatch.PatchMetaFromStruct)}, p, err + } + + patchMeta := strategicpatch.PatchMeta{} + patchMeta.SetPatchMergeKey(fieldPatchMergeKey) + patchMeta.SetPatchStrategies(fieldPatchStrategies) + return PatchMetaFromStruct{strategicpatch.PatchMetaFromStruct{T: fieldType}}, + patchMeta, nil +} diff --git a/pkg/devspace/config/loader/parse.go b/pkg/devspace/config/loader/parse.go index f16648ffeb..4b257f4f1c 100644 --- a/pkg/devspace/config/loader/parse.go +++ b/pkg/devspace/config/loader/parse.go @@ -117,6 +117,18 @@ func (l *configLoader) parseConfig(data map[interface{}]interface{}) (*latest.Co return nil, err } + // Apply merge + data, err = ApplyMerge(data, profiles[i]) + if err != nil { + return nil, err + } + + // Apply strategic merge + data, err = ApplyStrategicMerge(data, profiles[i]) + if err != nil { + return nil, err + } + // Apply patches data, err = ApplyPatches(data, profiles[i]) if err != nil { diff --git a/pkg/devspace/config/loader/parse_test.go b/pkg/devspace/config/loader/parse_test.go index 8cc2665685..57facf9f37 100644 --- a/pkg/devspace/config/loader/parse_test.go +++ b/pkg/devspace/config/loader/parse_test.go @@ -533,7 +533,7 @@ profiles: }, Images: map[string]*latest.ImageConfig{ "test": &latest.ImageConfig{ - Image: "test", + Image: "test", PreferSyncOverRebuild: true, }, }, @@ -570,6 +570,89 @@ profiles: }, expectedErr: true, }, + "Profile strategic merge": { + in: &parseTestCaseInput{ + config: ` +version: v1beta9 +images: + test: + image: test/test + delete: + image: test/test +deployments: +- name: test + helm: + values: + service: + ports: + - port: 3000 + containers: + - image: test/test + - image: test456/test456 +- name: test2 + helm: + values: + containers: + - image: test/test +profiles: +- name: test + strategicMerge: + images: + test: + image: test2/test2 + delete: null + deployments: + - name: test + helm: + values: + containers: + - image: test123/test123`, + options: &ConfigOptions{Profile: "test"}, + generatedConfig: &generated.Config{Vars: map[string]string{}}, + }, + expected: &latest.Config{ + Version: latest.Version, + Dev: &latest.DevConfig{}, + Images: map[string]*latest.ImageConfig{ + "test": { + Image: "test2/test2", + }, + }, + Deployments: []*latest.DeploymentConfig{ + { + Name: "test", + Helm: &latest.HelmConfig{ + Values: map[interface{}]interface{}{ + "service": map[interface{}]interface{}{ + "ports": []interface{}{ + map[interface{}]interface{}{ + "port": 3000, + }, + }, + }, + "containers": []interface{}{ + map[interface{}]interface{}{ + "image": "test123/test123", + }, + }, + }, + }, + }, + { + Name: "test2", + Helm: &latest.HelmConfig{ + Values: map[interface{}]interface{}{ + "containers": []interface{}{ + map[interface{}]interface{}{ + "image": "test/test", + }, + }, + }, + }, + }, + }, + }, + }, } // Execute test cases diff --git a/pkg/devspace/config/loader/predefined_vars.go b/pkg/devspace/config/loader/predefined_vars.go index cab9490c93..00968df69d 100644 --- a/pkg/devspace/config/loader/predefined_vars.go +++ b/pkg/devspace/config/loader/predefined_vars.go @@ -2,8 +2,10 @@ package loader import ( "bytes" + "encoding/json" "fmt" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl/util" + "os" "path/filepath" "strconv" "strings" @@ -72,10 +74,16 @@ func AddPredefinedVars(plugins []plugin.Metadata) { for _, variable := range p.Vars { v := variable predefinedVars[variable.Name] = func(configLoader *configLoader) (string, error) { + args, err := json.Marshal(os.Args) + if err != nil { + return "", err + } + buffer := &bytes.Buffer{} - err := plugin.CallPluginExecutable(filepath.Join(pluginFolder, plugin.PluginBinary), v.BaseArgs, map[string]string{ - "DEVSPACE_PLUGIN_KUBE_CONTEXT_FLAG": configLoader.options.KubeContext, - "DEVSPACE_PLUGIN_KUBE_NAMESPACE_FLAG": configLoader.options.Namespace, + err = plugin.CallPluginExecutable(filepath.Join(pluginFolder, plugin.PluginBinary), v.BaseArgs, map[string]string{ + plugin.KubeContextFlagEnv: configLoader.options.KubeContext, + plugin.KubeNamespaceFlagEnv: configLoader.options.Namespace, + plugin.OsArgsEnv: string(args), }, buffer) if err != nil { return "", fmt.Errorf("executing plugin %s: %s - %v", pluginName, buffer.String(), err) diff --git a/pkg/devspace/config/loader/validate.go b/pkg/devspace/config/loader/validate.go index bfef8362a3..e008fe4346 100644 --- a/pkg/devspace/config/loader/validate.go +++ b/pkg/devspace/config/loader/validate.go @@ -53,101 +53,99 @@ func validate(config *latest.Config) error { } } - if config.Commands != nil { - for index, command := range config.Commands { - if command.Name == "" { - return errors.Errorf("commands[%d].name is required", index) - } - if command.Command == "" { - return errors.Errorf("commands[%d].command is required", index) - } + for index, command := range config.Commands { + if command.Name == "" { + return errors.Errorf("commands[%d].name is required", index) + } + if command.Command == "" { + return errors.Errorf("commands[%d].command is required", index) } } - if config.Hooks != nil { - for index, hookConfig := range config.Hooks { - if hookConfig.Command == "" { - return errors.Errorf("hooks[%d].command is required", index) - } + for index, hookConfig := range config.Hooks { + if hookConfig.Command == "" { + return errors.Errorf("hooks[%d].command is required", index) } } - if config.Images != nil { - // images lists all the image names in order to check for duplicates - images := map[string]bool{} - for imageConfigName, imageConf := range config.Images { - if imageConfigName == "" { - return errors.Errorf("images keys cannot be an empty string") - } - if imageConf == nil { - return errors.Errorf("images.%s is empty and should at least contain an image name", imageConfigName) - } - if imageConf.Image == "" { - return errors.Errorf("images.%s.image is required", imageConfigName) - } - if imageConf.Build != nil && imageConf.Build.Custom != nil && imageConf.Build.Custom.Command == "" { - return errors.Errorf("images.%s.build.custom.command is required", imageConfigName) - } - if imageConf.Image == "" { - return fmt.Errorf("images.%s.image is required", imageConfigName) - } - if images[imageConf.Image] { - return errors.Errorf("multiple image definitions with the same image name are not allowed") - } - images[imageConf.Image] = true + // images lists all the image names in order to check for duplicates + images := map[string]bool{} + for imageConfigName, imageConf := range config.Images { + if imageConfigName == "" { + return errors.Errorf("images keys cannot be an empty string") + } + if imageConf == nil { + return errors.Errorf("images.%s is empty and should at least contain an image name", imageConfigName) + } + if imageConf.Image == "" { + return errors.Errorf("images.%s.image is required", imageConfigName) + } + if imageConf.Build != nil && imageConf.Build.Custom != nil && imageConf.Build.Custom.Command == "" { + return errors.Errorf("images.%s.build.custom.command is required", imageConfigName) } + if imageConf.Image == "" { + return fmt.Errorf("images.%s.image is required", imageConfigName) + } + if images[imageConf.Image] { + return errors.Errorf("multiple image definitions with the same image name are not allowed") + } + images[imageConf.Image] = true } - if config.Deployments != nil { - for index, deployConfig := range config.Deployments { - if deployConfig.Name == "" { - return errors.Errorf("deployments[%d].name is required", index) - } - if deployConfig.Helm == nil && deployConfig.Kubectl == nil { - return errors.Errorf("Please specify either helm or kubectl as deployment type in deployment %s", deployConfig.Name) - } - if deployConfig.Helm != nil && (deployConfig.Helm.Chart == nil || deployConfig.Helm.Chart.Name == "") && (deployConfig.Helm.ComponentChart == nil || *deployConfig.Helm.ComponentChart == false) { - return errors.Errorf("deployments[%d].helm.chart and deployments[%d].helm.chart.name or deployments[%d].helm.componentChart is required", index, index, index) - } - if deployConfig.Kubectl != nil && deployConfig.Kubectl.Manifests == nil { - return errors.Errorf("deployments[%d].kubectl.manifests is required", index) - } - if deployConfig.Helm != nil && deployConfig.Helm.ComponentChart != nil && *deployConfig.Helm.ComponentChart == true { - // Load override values from path - overwriteValues := map[interface{}]interface{}{} - if deployConfig.Helm.ValuesFiles != nil { - for _, overridePath := range deployConfig.Helm.ValuesFiles { - overwriteValuesPath, err := filepath.Abs(overridePath) - if err != nil { - return errors.Errorf("deployments[%d].helm.valuesFiles: Error retrieving absolute path from %s: %v", index, overridePath, err) - } - - overwriteValuesFromPath := map[interface{}]interface{}{} - err = yamlutil.ReadYamlFromFile(overwriteValuesPath, overwriteValuesFromPath) - if err == nil { - merge.Values(overwriteValues).MergeInto(overwriteValuesFromPath) - } + for index, deployConfig := range config.Deployments { + if deployConfig.Name == "" { + return errors.Errorf("deployments[%d].name is required", index) + } + if deployConfig.Helm == nil && deployConfig.Kubectl == nil { + return errors.Errorf("Please specify either helm or kubectl as deployment type in deployment %s", deployConfig.Name) + } + if deployConfig.Helm != nil && (deployConfig.Helm.Chart == nil || deployConfig.Helm.Chart.Name == "") && (deployConfig.Helm.ComponentChart == nil || *deployConfig.Helm.ComponentChart == false) { + return errors.Errorf("deployments[%d].helm.chart and deployments[%d].helm.chart.name or deployments[%d].helm.componentChart is required", index, index, index) + } + if deployConfig.Kubectl != nil && deployConfig.Kubectl.Manifests == nil { + return errors.Errorf("deployments[%d].kubectl.manifests is required", index) + } + if deployConfig.Helm != nil && deployConfig.Helm.ComponentChart != nil && *deployConfig.Helm.ComponentChart == true { + // Load override values from path + overwriteValues := map[interface{}]interface{}{} + if deployConfig.Helm.ValuesFiles != nil { + for _, overridePath := range deployConfig.Helm.ValuesFiles { + overwriteValuesPath, err := filepath.Abs(overridePath) + if err != nil { + return errors.Errorf("deployments[%d].helm.valuesFiles: Error retrieving absolute path from %s: %v", index, overridePath, err) } - } - // Load override values from data and merge them - if deployConfig.Helm.Values != nil { - merge.Values(overwriteValues).MergeInto(deployConfig.Helm.Values) + overwriteValuesFromPath := map[interface{}]interface{}{} + err = yamlutil.ReadYamlFromFile(overwriteValuesPath, overwriteValuesFromPath) + if err == nil { + merge.Values(overwriteValues).MergeInto(overwriteValuesFromPath) + } } + } - bytes, err := yaml.Marshal(overwriteValues) - if err != nil { - return errors.Errorf("deployments[%d].helm: Error marshaling overwrite values: %v", index, err) - } + // Load override values from data and merge them + if deployConfig.Helm.Values != nil { + merge.Values(overwriteValues).MergeInto(deployConfig.Helm.Values) + } - componentValues := &latest.ComponentConfig{} - err = yaml.UnmarshalStrict(bytes, componentValues) - if err != nil { - return errors.Errorf("deployments[%d].helm.componentChart: component values are incorrect: %v", index, err) - } + bytes, err := yaml.Marshal(overwriteValues) + if err != nil { + return errors.Errorf("deployments[%d].helm: Error marshaling overwrite values: %v", index, err) + } + + componentValues := &latest.ComponentConfig{} + err = yaml.UnmarshalStrict(bytes, componentValues) + if err != nil { + return errors.Errorf("deployments[%d].helm.componentChart: component values are incorrect: %v", index, err) } } } + for i, ps := range config.PullSecrets { + if ps.Registry == "" { + return errors.Errorf("pullSecrets[%d].registry: cannot be empty", i) + } + } + return nil } diff --git a/pkg/devspace/config/versions/latest/schema.go b/pkg/devspace/config/versions/latest/schema.go index 3fdc62a06c..81f0b98ab2 100644 --- a/pkg/devspace/config/versions/latest/schema.go +++ b/pkg/devspace/config/versions/latest/schema.go @@ -30,161 +30,163 @@ func NewRaw() *Config { type Config struct { Version string `yaml:"version"` - Images map[string]*ImageConfig `yaml:"images,omitempty"` - Deployments []*DeploymentConfig `yaml:"deployments,omitempty"` - Dev *DevConfig `yaml:"dev,omitempty"` - Dependencies []*DependencyConfig `yaml:"dependencies,omitempty"` - Hooks []*HookConfig `yaml:"hooks,omitempty"` - Commands []*CommandConfig `yaml:"commands,omitempty"` + Images map[string]*ImageConfig `yaml:"images,omitempty" json:"images,omitempty"` + Deployments []*DeploymentConfig `yaml:"deployments,omitempty" json:"deployments,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + Dev *DevConfig `yaml:"dev,omitempty" json:"dev,omitempty"` + Dependencies []*DependencyConfig `yaml:"dependencies,omitempty" json:"dependencies,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + Hooks []*HookConfig `yaml:"hooks,omitempty" json:"hooks,omitempty"` + PullSecrets []*PullSecretConfig `yaml:"pullSecrets,omitempty" json:"pullSecrets,omitempty" patchStrategy:"merge" patchMergeKey:"registry"` - Vars []*Variable `yaml:"vars,omitempty"` - Profiles []*ProfileConfig `yaml:"profiles,omitempty"` + Commands []*CommandConfig `yaml:"commands,omitempty" json:"commands,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + + Vars []*Variable `yaml:"vars,omitempty" json:"vars,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + Profiles []*ProfileConfig `yaml:"profiles,omitempty" json:"profiles,omitempty"` } // ImageConfig defines the image specification type ImageConfig struct { // Image is the complete image name including registry and repository // for example myregistry.com/mynamespace/myimage - Image string `yaml:"image"` + Image string `yaml:"image" json:"image"` // Tags is an array that specifes all tags that should be build during // the build process. If this is empty, devspace will generate a random tag - Tags []string `yaml:"tags,omitempty"` + Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` // Specifies a path (relative or absolute) to the dockerfile - Dockerfile string `yaml:"dockerfile,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` // The context path to build with - Context string `yaml:"context,omitempty"` + Context string `yaml:"context,omitempty" json:"context,omitempty"` // Entrypoint specifies an entrypoint that will be appended to the dockerfile during // image build in memory. Example: ["sleep", "99999"] - Entrypoint []string `yaml:"entrypoint,omitempty"` + Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"` // Cmd specifies the arguments for the entrypoint that will be appended // during build in memory to the dockerfile - Cmd []string `yaml:"cmd,omitempty"` + Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"` // CreatePullSecret specifies if a pull secret should be created for this image in the // target namespace. Defaults to true - CreatePullSecret *bool `yaml:"createPullSecret,omitempty"` + CreatePullSecret *bool `yaml:"createPullSecret,omitempty" json:"createPullSecret,omitempty"` // If this is true, devspace will not rebuild the image even though files have changed within // the context if a syncpath for this image is defined. This can reduce the number of builds // when running 'devspace dev' - PreferSyncOverRebuild bool `yaml:"preferSyncOverRebuild,omitempty"` + PreferSyncOverRebuild bool `yaml:"preferSyncOverRebuild,omitempty" json:"preferSyncOverRebuild,omitempty"` // If true injects a small restart script into the container and wraps the entrypoint of that // container, so that devspace is able to restart the complete container during sync. // Please make sure you either have an Entrypoint defined in the devspace config or in the // dockerfile for this image, otherwise devspace will fail. - InjectRestartHelper bool `yaml:"injectRestartHelper,omitempty"` + InjectRestartHelper bool `yaml:"injectRestartHelper,omitempty" json:"injectRestartHelper,omitempty"` // These instructions will be appended to the Dockerfile that is build at the current build target // and are appended before the entrypoint and cmd instructions - AppendDockerfileInstructions []string `yaml:"appendDockerfileInstructions,omitempty"` + AppendDockerfileInstructions []string `yaml:"appendDockerfileInstructions,omitempty" json:"appendDockerfileInstructions,omitempty"` // Specific build options how to build the specified image - Build *BuildConfig `yaml:"build,omitempty"` + Build *BuildConfig `yaml:"build,omitempty" json:"build,omitempty"` } // BuildConfig defines the build process for an image. Only one of the options below // can be specified. type BuildConfig struct { // If docker is specified, devspace will build the image using the local docker daemon - Docker *DockerConfig `yaml:"docker,omitempty"` + Docker *DockerConfig `yaml:"docker,omitempty" json:"docker,omitempty"` // If kaniko is specified, devspace will build the image in-cluster with kaniko - Kaniko *KanikoConfig `yaml:"kaniko,omitempty"` + Kaniko *KanikoConfig `yaml:"kaniko,omitempty" json:"kaniko,omitempty"` // If custom is specified, devspace will build the image with the help of // a custom script. - Custom *CustomConfig `yaml:"custom,omitempty"` + Custom *CustomConfig `yaml:"custom,omitempty" json:"custom,omitempty"` // This overrides other options and is able to disable the build for this image. // Useful if you just want to select the image in a sync path or via devspace enter --image - Disabled *bool `yaml:"disabled,omitempty"` + Disabled *bool `yaml:"disabled,omitempty" json:"disabled,omitempty"` } // DockerConfig tells the DevSpace CLI to build with Docker on Minikube or on localhost type DockerConfig struct { - PreferMinikube *bool `yaml:"preferMinikube,omitempty"` - SkipPush *bool `yaml:"skipPush,omitempty"` - DisableFallback *bool `yaml:"disableFallback,omitempty"` - UseBuildKit *bool `yaml:"useBuildKit,omitempty"` - Args []string `yaml:"args,omitempty"` - Options *BuildOptions `yaml:"options,omitempty"` + PreferMinikube *bool `yaml:"preferMinikube,omitempty" json:"preferMinikube,omitempty"` + SkipPush *bool `yaml:"skipPush,omitempty" json:"skipPush,omitempty"` + DisableFallback *bool `yaml:"disableFallback,omitempty" json:"disableFallback,omitempty"` + UseBuildKit *bool `yaml:"useBuildKit,omitempty" json:"useBuildKit,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + Options *BuildOptions `yaml:"options,omitempty" json:"options,omitempty"` } // KanikoConfig tells the DevSpace CLI to build with Docker on Minikube or on localhost type KanikoConfig struct { // if a cache repository should be used. defaults to true - Cache *bool `yaml:"cache,omitempty"` + Cache *bool `yaml:"cache,omitempty" json:"cache,omitempty"` // the snapshot mode kaniko should use. defaults to time - SnapshotMode string `yaml:"snapshotMode,omitempty"` + SnapshotMode string `yaml:"snapshotMode,omitempty" json:"snapshotMode,omitempty"` // the image name of the kaniko pod to use - Image string `yaml:"image,omitempty"` + Image string `yaml:"image,omitempty" json:"image,omitempty"` // additional arguments that should be passed to kaniko - Args []string `yaml:"args,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` // the namespace where the kaniko pod should be run - Namespace string `yaml:"namespace,omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` // if true pushing to insecure registries is allowed - Insecure *bool `yaml:"insecure,omitempty"` + Insecure *bool `yaml:"insecure,omitempty" json:"insecure,omitempty"` // the pull secret to mount by default - PullSecret string `yaml:"pullSecret,omitempty"` + PullSecret string `yaml:"pullSecret,omitempty" json:"pullSecret,omitempty"` // additional mounts that will be added to the build pod - AdditionalMounts []KanikoAdditionalMount `yaml:"additionalMounts,omitempty"` + AdditionalMounts []KanikoAdditionalMount `yaml:"additionalMounts,omitempty" json:"additionalMounts,omitempty"` // the resources that should be set on the kaniko pod - Resources *KanikoPodResources `yaml:"resources,omitempty"` + Resources *KanikoPodResources `yaml:"resources,omitempty" json:"resources,omitempty"` // other build options that will be passed to the kaniko pod - Options *BuildOptions `yaml:"options,omitempty"` + Options *BuildOptions `yaml:"options,omitempty" json:"options,omitempty"` } // KanikoPodResources describes the resources section of the started kaniko pod type KanikoPodResources struct { // The requests part of the resources - Requests map[string]string `yaml:"requests,omitempty"` + Requests map[string]string `yaml:"requests,omitempty" json:"requests,omitempty"` // The limits part of the resources - Limits map[string]string `yaml:"limits,omitempty"` + Limits map[string]string `yaml:"limits,omitempty" json:"limits,omitempty"` } // KanikoAdditionalMount tells devspace how the additional mount of the kaniko pod should look like type KanikoAdditionalMount struct { // The secret that should be mounted - Secret *KanikoAdditionalMountSecret `yaml:"secret,omitempty"` + Secret *KanikoAdditionalMountSecret `yaml:"secret,omitempty" json:"secret,omitempty"` // The configMap that should be mounted - ConfigMap *KanikoAdditionalMountConfigMap `yaml:"configMap,omitempty"` + ConfigMap *KanikoAdditionalMountConfigMap `yaml:"configMap,omitempty" json:"configMap,omitempty"` // Mounted read-only if true, read-write otherwise (false or unspecified). // Defaults to false. // +optional - ReadOnly bool `yaml:"readOnly,omitempty"` + ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"` // Path within the container at which the volume should be mounted. Must // not contain ':'. - MountPath string `yaml:"mountPath,omitempty"` + MountPath string `yaml:"mountPath,omitempty" json:"mountPath,omitempty"` // Path within the volume from which the container's volume should be mounted. // Defaults to "" (volume's root). // +optional - SubPath string `yaml:"subPath,omitempty"` + SubPath string `yaml:"subPath,omitempty" json:"subPath,omitempty"` } type KanikoAdditionalMountConfigMap struct { // Name of the configmap // +optional - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` // If unspecified, each key-value pair in the Data field of the referenced // ConfigMap will be projected into the volume as a file whose name is the @@ -194,7 +196,7 @@ type KanikoAdditionalMountConfigMap struct { // the volume setup will error unless it is marked optional. Paths must be // relative and may not contain the '..' path or start with '..'. // +optional - Items []KanikoAdditionalMountKeyToPath `yaml:"items,omitempty"` + Items []KanikoAdditionalMountKeyToPath `yaml:"items,omitempty" json:"items,omitempty"` // Optional: mode bits to use on created files by default. Must be a // value between 0 and 0777. Defaults to 0644. @@ -202,14 +204,14 @@ type KanikoAdditionalMountConfigMap struct { // This might be in conflict with other options that affect the file // mode, like fsGroup, and the result can be other mode bits set. // +optional - DefaultMode *int32 `yaml:"defaultMode,omitempty"` + DefaultMode *int32 `yaml:"defaultMode,omitempty" json:"defaultMode,omitempty"` } type KanikoAdditionalMountSecret struct { // Name of the secret in the pod's namespace to use. // More info: https://kubernetes.io/docs/concepts/storage/volumes#secret // +optional - Name string `yaml:"name"` + Name string `yaml:"name" json:"name"` // If unspecified, each key-value pair in the Data field of the referenced // Secret will be projected into the volume as a file whose name is the @@ -219,7 +221,7 @@ type KanikoAdditionalMountSecret struct { // the volume setup will error unless it is marked optional. Paths must be // relative and may not contain the '..' path or start with '..'. // +optional - Items []KanikoAdditionalMountKeyToPath `yaml:"items,omitempty"` + Items []KanikoAdditionalMountKeyToPath `yaml:"items,omitempty" json:"items,omitempty"` // Optional: mode bits to use on created files by default. Must be a // value between 0 and 0777. Defaults to 0644. @@ -227,328 +229,328 @@ type KanikoAdditionalMountSecret struct { // This might be in conflict with other options that affect the file // mode, like fsGroup, and the result can be other mode bits set. // +optional - DefaultMode *int32 `yaml:"defaultMode,omitempty"` + DefaultMode *int32 `yaml:"defaultMode,omitempty" json:"defaultMode,omitempty"` } type KanikoAdditionalMountKeyToPath struct { // The key to project. - Key string `yaml:"key"` + Key string `yaml:"key" json:"key"` // The relative path of the file to map the key to. // May not be an absolute path. // May not contain the path element '..'. // May not start with the string '..'. - Path string `yaml:"path"` + Path string `yaml:"path" json:"path"` // Optional: mode bits to use on this file, must be a value between 0 // and 0777. If not specified, the volume defaultMode will be used. // This might be in conflict with other options that affect the file // mode, like fsGroup, and the result can be other mode bits set. // +optional - Mode *int32 `yaml:"mode,omitempty"` + Mode *int32 `yaml:"mode,omitempty" json:"mode,omitempty"` } // CustomConfig tells the DevSpace CLI to build with a custom build script type CustomConfig struct { - Command string `yaml:"command,omitempty"` - AppendArgs []string `yaml:"appendArgs,omitempty"` - Args []string `yaml:"args,omitempty"` - ImageFlag string `yaml:"imageFlag,omitempty"` - OnChange []string `yaml:"onChange,omitempty"` + Command string `yaml:"command,omitempty" json:"command,omitempty"` + AppendArgs []string `yaml:"appendArgs,omitempty" json:"appendArgs,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + ImageFlag string `yaml:"imageFlag,omitempty" json:"imageFlag,omitempty"` + OnChange []string `yaml:"onChange,omitempty" json:"onChange,omitempty"` } // BuildOptions defines options for building Docker images type BuildOptions struct { - Target string `yaml:"target,omitempty"` - Network string `yaml:"network,omitempty"` - BuildArgs map[string]*string `yaml:"buildArgs,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Network string `yaml:"network,omitempty" json:"network,omitempty"` + BuildArgs map[string]*string `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` } // DeploymentConfig defines the configuration how the devspace should be deployed type DeploymentConfig struct { - Name string `yaml:"name"` - Namespace string `yaml:"namespace,omitempty"` - Helm *HelmConfig `yaml:"helm,omitempty"` - Kubectl *KubectlConfig `yaml:"kubectl,omitempty"` + Name string `yaml:"name" json:"name"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` + Helm *HelmConfig `yaml:"helm,omitempty" json:"helm,omitempty"` + Kubectl *KubectlConfig `yaml:"kubectl,omitempty" json:"kubectl,omitempty"` } // ComponentConfig holds the component information type ComponentConfig struct { - InitContainers []*ContainerConfig `yaml:"initContainers,omitempty"` - Containers []*ContainerConfig `yaml:"containers,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty"` - Volumes []*VolumeConfig `yaml:"volumes,omitempty"` - Service *ServiceConfig `yaml:"service,omitempty"` - ServiceName string `yaml:"serviceName,omitempty"` - Ingress *IngressConfig `yaml:"ingress,omitempty"` - Replicas *int `yaml:"replicas,omitempty"` - Autoscaling *AutoScalingConfig `yaml:"autoScaling,omitempty"` - RollingUpdate *RollingUpdateConfig `yaml:"rollingUpdate,omitempty"` - PullSecrets []*string `yaml:"pullSecrets,omitempty"` - Tolerations []map[interface{}]interface{} `yaml:"tolerations,omitempty"` - Affinity map[interface{}]interface{} `yaml:"affinity,omitempty"` - NodeSelector map[interface{}]interface{} `yaml:"nodeSelector,omitempty"` - NodeName string `yaml:"nodeName,omitempty"` - PodManagementPolicy string `yaml:"podManagementPolicy,omitempty"` - - DNSConfig map[interface{}]interface{} `yaml:"dnsConfig,omitempty"` - HostAliases []map[interface{}]interface{} `yaml:"hostAliases,omitempty"` - Overhead map[interface{}]interface{} `yaml:"overhead,omitempty"` - ReadinessGates []map[interface{}]interface{} `yaml:"readinessGates,omitempty"` - SecurityContext map[interface{}]interface{} `yaml:"securityContext,omitempty"` - TopologySpreadConstraints []map[interface{}]interface{} `yaml:"topologySpreadConstraints,omitempty"` - ActiveDeadlineSeconds *int `yaml:"activeDeadlineSeconds,omitempty"` - AutomountServiceAccountToken *bool `yaml:"automountServiceAccountToken,omitempty"` - DnsPolicy *string `yaml:"dnsPolicy,omitempty"` - EnableServiceLinks *bool `yaml:"enableServiceLinks,omitempty"` - HostIPC *bool `yaml:"hostIPC,omitempty"` - HostNetwork *bool `yaml:"hostNetwork,omitempty"` - HostPID *bool `yaml:"hostPID,omitempty"` - Hostname *string `yaml:"hostname,omitempty"` - PreemptionPolicy *string `yaml:"preemptionPolicy,omitempty"` - Priority *int `yaml:"priority,omitempty"` - PriorityClassName *string `yaml:"priorityClassName,omitempty"` - RestartPolicy *string `yaml:"restartPolicy,omitempty"` - RuntimeClassName *string `yaml:"runtimeClassName,omitempty"` - SchedulerName *string `yaml:"schedulerName,omitempty"` - ServiceAccount *string `yaml:"serviceAccount,omitempty"` - ServiceAccountName *string `yaml:"serviceAccountName,omitempty"` - SetHostnameAsFQDN *bool `yaml:"setHostnameAsFQDN,omitempty"` - ShareProcessNamespace *bool `yaml:"shareProcessNamespace,omitempty"` - Subdomain *string `yaml:"subdomain,omitempty"` - TerminationGracePeriodSeconds *int `yaml:"terminationGracePeriodSeconds,omitempty"` - EphemeralContainers []map[interface{}]interface{} `yaml:"ephemeralContainers,omitempty"` + InitContainers []*ContainerConfig `yaml:"initContainers,omitempty" json:"initContainers,omitempty"` + Containers []*ContainerConfig `yaml:"containers,omitempty" json:"containers,omitempty"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + Volumes []*VolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"` + Service *ServiceConfig `yaml:"service,omitempty" json:"service,omitempty"` + ServiceName string `yaml:"serviceName,omitempty" json:"serviceName,omitempty"` + Ingress *IngressConfig `yaml:"ingress,omitempty" json:"ingress,omitempty"` + Replicas *int `yaml:"replicas,omitempty" json:"replicas,omitempty"` + Autoscaling *AutoScalingConfig `yaml:"autoScaling,omitempty" json:"autoScaling,omitempty"` + RollingUpdate *RollingUpdateConfig `yaml:"rollingUpdate,omitempty" json:"rollingUpdate,omitempty"` + PullSecrets []*string `yaml:"pullSecrets,omitempty" json:"pullSecrets,omitempty"` + Tolerations []map[interface{}]interface{} `yaml:"tolerations,omitempty" json:"tolerations,omitempty"` + Affinity map[interface{}]interface{} `yaml:"affinity,omitempty" json:"affinity,omitempty"` + NodeSelector map[interface{}]interface{} `yaml:"nodeSelector,omitempty" json:"nodeSelector,omitempty"` + NodeName string `yaml:"nodeName,omitempty" json:"nodeName,omitempty"` + PodManagementPolicy string `yaml:"podManagementPolicy,omitempty" json:"podManagementPolicy,omitempty"` + + DNSConfig map[interface{}]interface{} `yaml:"dnsConfig,omitempty" json:"dnsConfig,omitempty"` + HostAliases []map[interface{}]interface{} `yaml:"hostAliases,omitempty" json:"hostAliases,omitempty"` + Overhead map[interface{}]interface{} `yaml:"overhead,omitempty" json:"overhead,omitempty"` + ReadinessGates []map[interface{}]interface{} `yaml:"readinessGates,omitempty" json:"readinessGates,omitempty"` + SecurityContext map[interface{}]interface{} `yaml:"securityContext,omitempty" json:"securityContext,omitempty"` + TopologySpreadConstraints []map[interface{}]interface{} `yaml:"topologySpreadConstraints,omitempty" json:"topologySpreadConstraints,omitempty"` + ActiveDeadlineSeconds *int `yaml:"activeDeadlineSeconds,omitempty" json:"activeDeadlineSeconds,omitempty"` + AutomountServiceAccountToken *bool `yaml:"automountServiceAccountToken,omitempty" json:"automountServiceAccountToken,omitempty"` + DnsPolicy *string `yaml:"dnsPolicy,omitempty" json:"dnsPolicy,omitempty"` + EnableServiceLinks *bool `yaml:"enableServiceLinks,omitempty" json:"enableServiceLinks,omitempty"` + HostIPC *bool `yaml:"hostIPC,omitempty" json:"hostIPC,omitempty"` + HostNetwork *bool `yaml:"hostNetwork,omitempty" json:"hostNetwork,omitempty"` + HostPID *bool `yaml:"hostPID,omitempty" json:"hostPID,omitempty"` + Hostname *string `yaml:"hostname,omitempty" json:"hostname,omitempty"` + PreemptionPolicy *string `yaml:"preemptionPolicy,omitempty" json:"preemptionPolicy,omitempty"` + Priority *int `yaml:"priority,omitempty" json:"priority,omitempty"` + PriorityClassName *string `yaml:"priorityClassName,omitempty" json:"priorityClassName,omitempty"` + RestartPolicy *string `yaml:"restartPolicy,omitempty" json:"restartPolicy,omitempty"` + RuntimeClassName *string `yaml:"runtimeClassName,omitempty" json:"runtimeClassName,omitempty"` + SchedulerName *string `yaml:"schedulerName,omitempty" json:"schedulerName,omitempty"` + ServiceAccount *string `yaml:"serviceAccount,omitempty" json:"serviceAccount,omitempty"` + ServiceAccountName *string `yaml:"serviceAccountName,omitempty" json:"serviceAccountName,omitempty"` + SetHostnameAsFQDN *bool `yaml:"setHostnameAsFQDN,omitempty" json:"setHostnameAsFQDN,omitempty"` + ShareProcessNamespace *bool `yaml:"shareProcessNamespace,omitempty" json:"shareProcessNamespace,omitempty"` + Subdomain *string `yaml:"subdomain,omitempty" json:"subdomain,omitempty"` + TerminationGracePeriodSeconds *int `yaml:"terminationGracePeriodSeconds,omitempty" json:"terminationGracePeriodSeconds,omitempty"` + EphemeralContainers []map[interface{}]interface{} `yaml:"ephemeralContainers,omitempty" json:"ephemeralContainers,omitempty"` } // ContainerConfig holds the configurations of a container type ContainerConfig struct { - Name string `yaml:"name,omitempty"` - Image string `yaml:"image,omitempty"` - Command []string `yaml:"command,omitempty"` - Args []string `yaml:"args,omitempty"` - Stdin bool `yaml:"stdin,omitempty"` - TTY bool `yaml:"tty,omitempty"` - Env []map[interface{}]interface{} `yaml:"env,omitempty"` - EnvFrom []map[interface{}]interface{} `yaml:"envFrom,omitempty"` - VolumeMounts []*VolumeMountConfig `yaml:"volumeMounts,omitempty"` - Resources map[interface{}]interface{} `yaml:"resources,omitempty"` - LivenessProbe map[interface{}]interface{} `yaml:"livenessProbe,omitempty"` - ReadinessProbe map[interface{}]interface{} `yaml:"readinessProbe,omitempty"` - StartupProbe map[interface{}]interface{} `yaml:"startupProbe,omitempty"` - SecurityContext map[interface{}]interface{} `yaml:"securityContext,omitempty"` - Lifecycle map[interface{}]interface{} `yaml:"lifecycle,omitempty"` - VolumeDevices []map[interface{}]interface{} `yaml:"volumeDevices,omitempty"` - ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"` - WorkingDir string `yaml:"workingDir,omitempty"` - StdinOnce bool `yaml:"stdinOnce,omitempty"` - TerminationMessagePath string `yaml:"terminationMessagePath,omitempty"` - TerminationMessagePolicy string `yaml:"terminationMessagePolicy,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Image string `yaml:"image,omitempty" json:"image,omitempty"` + Command []string `yaml:"command,omitempty" json:"command,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + Stdin bool `yaml:"stdin,omitempty" json:"stdin,omitempty"` + TTY bool `yaml:"tty,omitempty" json:"tty,omitempty"` + Env []map[interface{}]interface{} `yaml:"env,omitempty" json:"env,omitempty"` + EnvFrom []map[interface{}]interface{} `yaml:"envFrom,omitempty" json:"envFrom,omitempty"` + VolumeMounts []*VolumeMountConfig `yaml:"volumeMounts,omitempty" json:"volumeMounts,omitempty"` + Resources map[interface{}]interface{} `yaml:"resources,omitempty" json:"resources,omitempty"` + LivenessProbe map[interface{}]interface{} `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"` + ReadinessProbe map[interface{}]interface{} `yaml:"readinessProbe,omitempty" json:"readinessProbe,omitempty"` + StartupProbe map[interface{}]interface{} `yaml:"startupProbe,omitempty" json:"startupProbe,omitempty"` + SecurityContext map[interface{}]interface{} `yaml:"securityContext,omitempty" json:"securityContext,omitempty"` + Lifecycle map[interface{}]interface{} `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"` + VolumeDevices []map[interface{}]interface{} `yaml:"volumeDevices,omitempty" json:"volumeDevices,omitempty"` + ImagePullPolicy string `yaml:"imagePullPolicy,omitempty" json:"imagePullPolicy,omitempty"` + WorkingDir string `yaml:"workingDir,omitempty" json:"workingDir,omitempty"` + StdinOnce bool `yaml:"stdinOnce,omitempty" json:"stdinOnce,omitempty"` + TerminationMessagePath string `yaml:"terminationMessagePath,omitempty" json:"terminationMessagePath,omitempty"` + TerminationMessagePolicy string `yaml:"terminationMessagePolicy,omitempty" json:"terminationMessagePolicy,omitempty"` } // VolumeMountConfig holds the configuration for a specific mount path type VolumeMountConfig struct { - ContainerPath string `yaml:"containerPath,omitempty"` - Volume *VolumeMountVolumeConfig `yaml:"volume,omitempty"` + ContainerPath string `yaml:"containerPath,omitempty" json:"containerPath,omitempty"` + Volume *VolumeMountVolumeConfig `yaml:"volume,omitempty" json:"volume,omitempty"` } // VolumeMountVolumeConfig holds the configuration for a specfic mount path volume type VolumeMountVolumeConfig struct { - Name string `yaml:"name,omitempty"` - SubPath string `yaml:"subPath,omitempty"` - ReadOnly *bool `yaml:"readOnly,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + SubPath string `yaml:"subPath,omitempty" json:"subPath,omitempty"` + ReadOnly *bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"` } // VolumeConfig holds the configuration for a specific volume type VolumeConfig struct { - Name string `yaml:"name,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty"` - Size string `yaml:"size,omitempty"` - ConfigMap map[interface{}]interface{} `yaml:"configMap,omitempty"` - Secret map[interface{}]interface{} `yaml:"secret,omitempty"` - StorageClassName string `yaml:"storageClassName,omitempty"` - VolumeMode string `yaml:"volumeMode,omitempty"` - VolumeName string `yaml:"volumeName,omitempty"` - DataSource map[interface{}]interface{} `yaml:"dataSource,omitempty"` - AccessModes []string `yaml:"accessModes,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + Size string `yaml:"size,omitempty" json:"size,omitempty"` + ConfigMap map[interface{}]interface{} `yaml:"configMap,omitempty" json:"configMap,omitempty"` + Secret map[interface{}]interface{} `yaml:"secret,omitempty" json:"secret,omitempty"` + StorageClassName string `yaml:"storageClassName,omitempty" json:"storageClassName,omitempty"` + VolumeMode string `yaml:"volumeMode,omitempty" json:"volumeMode,omitempty"` + VolumeName string `yaml:"volumeName,omitempty" json:"volumeName,omitempty"` + DataSource map[interface{}]interface{} `yaml:"dataSource,omitempty" json:"dataSource,omitempty"` + AccessModes []string `yaml:"accessModes,omitempty" json:"accessModes,omitempty"` } // ServiceConfig holds the configuration of a component service type ServiceConfig struct { - Name string `yaml:"name,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty"` - Type string `yaml:"type,omitempty"` - Ports []*ServicePortConfig `yaml:"ports,omitempty"` - ExternalIPs []string `yaml:"externalIPs,omitempty"` - ClusterIP string `yaml:"clusterIP,omitempty"` - ExternalName string `yaml:"externalName,omitempty"` - ExternalTrafficPolicy string `yaml:"externalTrafficPolicy,omitempty"` - HealthCheckNodePort int `yaml:"healthCheckNodePort,omitempty"` - IpFamily *string `yaml:"ipFamily,omitempty"` - LoadBalancerIP *string `yaml:"loadBalancerIP,omitempty"` - LoadBalancerSourceRanges []string `yaml:"loadBalancerSourceRanges,omitempty"` - PublishNotReadyAddresses bool `yaml:"publishNotReadyAddresses,omitempty"` - SessionAffinity map[interface{}]interface{} `yaml:"sessionAffinity,omitempty"` - SessionAffinityConfig map[interface{}]interface{} `yaml:"sessionAffinityConfig,omitempty"` - TopologyKeys []string `yaml:"topologyKeys,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + Type string `yaml:"type,omitempty" json:"type,omitempty"` + Ports []*ServicePortConfig `yaml:"ports,omitempty" json:"ports,omitempty"` + ExternalIPs []string `yaml:"externalIPs,omitempty" json:"externalIPs,omitempty"` + ClusterIP string `yaml:"clusterIP,omitempty" json:"clusterIP,omitempty"` + ExternalName string `yaml:"externalName,omitempty" json:"externalName,omitempty"` + ExternalTrafficPolicy string `yaml:"externalTrafficPolicy,omitempty" json:"externalTrafficPolicy,omitempty"` + HealthCheckNodePort int `yaml:"healthCheckNodePort,omitempty" json:"healthCheckNodePort,omitempty"` + IpFamily *string `yaml:"ipFamily,omitempty" json:"ipFamily,omitempty"` + LoadBalancerIP *string `yaml:"loadBalancerIP,omitempty" json:"loadBalancerIP,omitempty"` + LoadBalancerSourceRanges []string `yaml:"loadBalancerSourceRanges,omitempty" json:"loadBalancerSourceRanges,omitempty"` + PublishNotReadyAddresses bool `yaml:"publishNotReadyAddresses,omitempty" json:"publishNotReadyAddresses,omitempty"` + SessionAffinity map[interface{}]interface{} `yaml:"sessionAffinity,omitempty" json:"sessionAffinity,omitempty"` + SessionAffinityConfig map[interface{}]interface{} `yaml:"sessionAffinityConfig,omitempty" json:"sessionAffinityConfig,omitempty"` + TopologyKeys []string `yaml:"topologyKeys,omitempty" json:"topologyKeys,omitempty"` } // ServicePortConfig holds the port configuration of a component service type ServicePortConfig struct { - Port *int `yaml:"port,omitempty"` - ContainerPort *int `yaml:"containerPort,omitempty"` - Protocol string `yaml:"protocol,omitempty"` + Port *int `yaml:"port,omitempty" json:"port,omitempty"` + ContainerPort *int `yaml:"containerPort,omitempty" json:"containerPort,omitempty"` + Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` } // IngressConfig holds the configuration of a component ingress type IngressConfig struct { - Name string `yaml:"name,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty"` - TLS string `yaml:"tls,omitempty"` - TLSClusterIssuer string `yaml:"tlsClusterIssuer,omitempty"` - IngressClass string `yaml:"ingressClass,omitempty"` - Rules []*IngressRuleConfig `yaml:"rules,omitempty"` - Backend map[interface{}]interface{} `yaml:"backend,omitempty"` - IngressClassName *string `yaml:"ingressClassName,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + TLS string `yaml:"tls,omitempty" json:"tls,omitempty"` + TLSClusterIssuer string `yaml:"tlsClusterIssuer,omitempty" json:"tlsClusterIssuer,omitempty"` + IngressClass string `yaml:"ingressClass,omitempty" json:"ingressClass,omitempty"` + Rules []*IngressRuleConfig `yaml:"rules,omitempty" json:"rules,omitempty"` + Backend map[interface{}]interface{} `yaml:"backend,omitempty" json:"backend,omitempty"` + IngressClassName *string `yaml:"ingressClassName,omitempty" json:"ingressClassName,omitempty"` } // IngressRuleConfig holds the port configuration of a component service type IngressRuleConfig struct { - Host string `yaml:"host,omitempty"` - TLS string `yaml:"tls,omitempty"` // DEPRECATED - Path string `yaml:"path,omitempty"` - ServicePort *int `yaml:"servicePort,omitempty"` - ServiceName string `yaml:"serviceName,omitempty"` + Host string `yaml:"host,omitempty" json:"host,omitempty"` + TLS string `yaml:"tls,omitempty" json:"tls,omitempty"` // DEPRECATED + Path string `yaml:"path,omitempty" json:"path,omitempty"` + ServicePort *int `yaml:"servicePort,omitempty" json:"servicePort,omitempty"` + ServiceName string `yaml:"serviceName,omitempty" json:"serviceName,omitempty"` } // AutoScalingConfig holds the autoscaling config of a component type AutoScalingConfig struct { - Horizontal *AutoScalingHorizontalConfig `yaml:"horizontal,omitempty"` + Horizontal *AutoScalingHorizontalConfig `yaml:"horizontal,omitempty" json:"horizontal,omitempty"` } // AutoScalingHorizontalConfig holds the horizontal autoscaling config of a component type AutoScalingHorizontalConfig struct { - MaxReplicas *int `yaml:"maxReplicas,omitempty"` - AverageCPU string `yaml:"averageCPU,omitempty"` - AverageRelativeCPU string `yaml:"averageRelativeCPU,omitempty"` - AverageMemory string `yaml:"averageMemory,omitempty"` - AverageRelativeMemory string `yaml:"averageRelativeMemory,omitempty"` + MaxReplicas *int `yaml:"maxReplicas,omitempty" json:"maxReplicas,omitempty"` + AverageCPU string `yaml:"averageCPU,omitempty" json:"averageCPU,omitempty"` + AverageRelativeCPU string `yaml:"averageRelativeCPU,omitempty" json:"averageRelativeCPU,omitempty"` + AverageMemory string `yaml:"averageMemory,omitempty" json:"averageMemory,omitempty"` + AverageRelativeMemory string `yaml:"averageRelativeMemory,omitempty" json:"averageRelativeMemory,omitempty"` } // RollingUpdateConfig holds the configuration for rolling updates type RollingUpdateConfig struct { - Enabled *bool `yaml:"enabled,omitempty"` - MaxSurge string `yaml:"maxSurge,omitempty"` - MaxUnavailable string `yaml:"maxUnavailable,omitempty"` - Partition *int `yaml:"partition,omitempty"` + Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + MaxSurge string `yaml:"maxSurge,omitempty" json:"maxSurge,omitempty"` + MaxUnavailable string `yaml:"maxUnavailable,omitempty" json:"maxUnavailable,omitempty"` + Partition *int `yaml:"partition,omitempty" json:"partition,omitempty"` } // HelmConfig defines the specific helm options used during deployment type HelmConfig struct { - Chart *ChartConfig `yaml:"chart,omitempty"` - ComponentChart *bool `yaml:"componentChart,omitempty"` - Values map[interface{}]interface{} `yaml:"values,omitempty"` - ValuesFiles []string `yaml:"valuesFiles,omitempty"` - ReplaceImageTags *bool `yaml:"replaceImageTags,omitempty"` - Wait bool `yaml:"wait,omitempty"` - Timeout *int64 `yaml:"timeout,omitempty"` - Force bool `yaml:"force,omitempty"` - Atomic bool `yaml:"atomic,omitempty"` - CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"` - Recreate bool `yaml:"recreate,omitempty"` - DisableHooks bool `yaml:"disableHooks,omitempty"` - Driver string `yaml:"driver,omitempty"` - Path string `yaml:"path,omitempty"` - V2 bool `yaml:"v2,omitempty"` - TillerNamespace string `yaml:"tillerNamespace,omitempty"` - - DeleteArgs []string `yaml:"deleteArgs,omitempty"` - TemplateArgs []string `yaml:"templateArgs,omitempty"` - UpgradeArgs []string `yaml:"upgradeArgs,omitempty"` - FetchArgs []string `yaml:"fetchArgs,omitempty"` + Chart *ChartConfig `yaml:"chart,omitempty" json:"chart,omitempty"` + ComponentChart *bool `yaml:"componentChart,omitempty" json:"componentChart,omitempty"` + Values map[interface{}]interface{} `yaml:"values,omitempty" json:"values,omitempty"` + ValuesFiles []string `yaml:"valuesFiles,omitempty" json:"valuesFiles,omitempty"` + ReplaceImageTags *bool `yaml:"replaceImageTags,omitempty" json:"replaceImageTags,omitempty"` + Wait bool `yaml:"wait,omitempty" json:"wait,omitempty"` + Timeout *int64 `yaml:"timeout,omitempty" json:"timeout,omitempty"` + Force bool `yaml:"force,omitempty" json:"force,omitempty"` + Atomic bool `yaml:"atomic,omitempty" json:"atomic,omitempty"` + CleanupOnFail bool `yaml:"cleanupOnFail,omitempty" json:"cleanupOnFail,omitempty"` + Recreate bool `yaml:"recreate,omitempty" json:"recreate,omitempty"` + DisableHooks bool `yaml:"disableHooks,omitempty" json:"disableHooks,omitempty"` + Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + V2 bool `yaml:"v2,omitempty" json:"v2,omitempty"` + TillerNamespace string `yaml:"tillerNamespace,omitempty" json:"tillerNamespace,omitempty"` + + DeleteArgs []string `yaml:"deleteArgs,omitempty" json:"deleteArgs,omitempty"` + TemplateArgs []string `yaml:"templateArgs,omitempty" json:"templateArgs,omitempty"` + UpgradeArgs []string `yaml:"upgradeArgs,omitempty" json:"upgradeArgs,omitempty"` + FetchArgs []string `yaml:"fetchArgs,omitempty" json:"fetchArgs,omitempty"` } // ChartConfig defines the helm chart options type ChartConfig struct { - Name string `yaml:"name,omitempty"` - Version string `yaml:"version,omitempty"` - RepoURL string `yaml:"repo,omitempty"` - Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` + RepoURL string `yaml:"repo,omitempty" json:"repo,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + Password string `yaml:"password,omitempty" json:"password,omitempty"` } // KubectlConfig defines the specific kubectl options used during deployment type KubectlConfig struct { - Manifests []string `yaml:"manifests,omitempty"` - Kustomize *bool `yaml:"kustomize,omitempty"` - KustomizeArgs []string `yaml:"kustomizeArgs,omitempty"` - ReplaceImageTags *bool `yaml:"replaceImageTags,omitempty"` - DeleteArgs []string `yaml:"deleteArgs,omitempty"` - CreateArgs []string `yaml:"createArgs,omitempty"` - ApplyArgs []string `yaml:"applyArgs,omitempty"` - CmdPath string `yaml:"cmdPath,omitempty"` + Manifests []string `yaml:"manifests,omitempty" json:"manifests,omitempty"` + Kustomize *bool `yaml:"kustomize,omitempty" json:"kustomize,omitempty"` + KustomizeArgs []string `yaml:"kustomizeArgs,omitempty" json:"kustomizeArgs,omitempty"` + ReplaceImageTags *bool `yaml:"replaceImageTags,omitempty" json:"replaceImageTags,omitempty"` + DeleteArgs []string `yaml:"deleteArgs,omitempty" json:"deleteArgs,omitempty"` + CreateArgs []string `yaml:"createArgs,omitempty" json:"createArgs,omitempty"` + ApplyArgs []string `yaml:"applyArgs,omitempty" json:"applyArgs,omitempty"` + CmdPath string `yaml:"cmdPath,omitempty" json:"cmdPath,omitempty"` } // DevConfig defines the devspace deployment type DevConfig struct { - Ports []*PortForwardingConfig `yaml:"ports,omitempty"` - Open []*OpenConfig `yaml:"open,omitempty"` - Sync []*SyncConfig `yaml:"sync,omitempty"` - Logs *LogsConfig `yaml:"logs,omitempty"` - AutoReload *AutoReloadConfig `yaml:"autoReload,omitempty"` - Interactive *InteractiveConfig `yaml:"interactive,omitempty"` + Ports []*PortForwardingConfig `yaml:"ports,omitempty" json:"ports,omitempty"` + Open []*OpenConfig `yaml:"open,omitempty" json:"open,omitempty"` + Sync []*SyncConfig `yaml:"sync,omitempty" json:"sync,omitempty" patchStrategy:"merge" patchMergeKey:"localSubPath"` + Logs *LogsConfig `yaml:"logs,omitempty" json:"logs,omitempty"` + AutoReload *AutoReloadConfig `yaml:"autoReload,omitempty" json:"autoReload,omitempty"` + Interactive *InteractiveConfig `yaml:"interactive,omitempty" json:"interactive,omitempty"` } // PortForwardingConfig defines the ports for a port forwarding to a DevSpace type PortForwardingConfig struct { - ImageName string `yaml:"imageName,omitempty"` - LabelSelector map[string]string `yaml:"labelSelector,omitempty"` - ContainerName string `yaml:"containerName,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - PortMappings []*PortMapping `yaml:"forward,omitempty"` - PortMappingsReverse []*PortMapping `yaml:"reverseForward,omitempty"` + ImageName string `yaml:"imageName,omitempty" json:"imageName,omitempty"` + LabelSelector map[string]string `yaml:"labelSelector,omitempty" json:"labelSelector,omitempty"` + ContainerName string `yaml:"containerName,omitempty" json:"containerName,omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` + PortMappings []*PortMapping `yaml:"forward,omitempty" json:"forward,omitempty"` + PortMappingsReverse []*PortMapping `yaml:"reverseForward,omitempty" json:"reverseForward,omitempty"` } // PortMapping defines the ports for a PortMapping type PortMapping struct { - LocalPort *int `yaml:"port"` - RemotePort *int `yaml:"remotePort,omitempty"` - BindAddress string `yaml:"bindAddress,omitempty"` + LocalPort *int `yaml:"port" json:"port"` + RemotePort *int `yaml:"remotePort,omitempty" json:"remotePort,omitempty"` + BindAddress string `yaml:"bindAddress,omitempty" json:"bindAddress,omitempty"` } // OpenConfig defines what to open after services have been started type OpenConfig struct { - URL string `yaml:"url,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` } // SyncConfig defines the paths for a SyncFolder type SyncConfig struct { - ImageName string `yaml:"imageName,omitempty"` - LabelSelector map[string]string `yaml:"labelSelector,omitempty"` - ContainerName string `yaml:"containerName,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - LocalSubPath string `yaml:"localSubPath,omitempty"` - ContainerPath string `yaml:"containerPath,omitempty"` - ExcludePaths []string `yaml:"excludePaths,omitempty"` - DownloadExcludePaths []string `yaml:"downloadExcludePaths,omitempty"` - UploadExcludePaths []string `yaml:"uploadExcludePaths,omitempty"` - InitialSync InitialSyncStrategy `yaml:"initialSync,omitempty"` - InitialSyncCompareBy InitialSyncCompareBy `yaml:"initialSyncCompareBy,omitempty"` - - DisableDownload *bool `yaml:"disableDownload,omitempty"` - DisableUpload *bool `yaml:"disableUpload,omitempty"` - - WaitInitialSync *bool `yaml:"waitInitialSync,omitempty"` - BandwidthLimits *BandwidthLimits `yaml:"bandwidthLimits,omitempty"` + ImageName string `yaml:"imageName,omitempty" json:"imageName,omitempty"` + LabelSelector map[string]string `yaml:"labelSelector,omitempty" json:"labelSelector,omitempty"` + ContainerName string `yaml:"containerName,omitempty" json:"containerName,omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` + LocalSubPath string `yaml:"localSubPath,omitempty" json:"localSubPath,omitempty"` + ContainerPath string `yaml:"containerPath,omitempty" json:"containerPath,omitempty"` + ExcludePaths []string `yaml:"excludePaths,omitempty" json:"excludePaths,omitempty"` + DownloadExcludePaths []string `yaml:"downloadExcludePaths,omitempty" json:"downloadExcludePaths,omitempty"` + UploadExcludePaths []string `yaml:"uploadExcludePaths,omitempty" json:"uploadExcludePaths,omitempty"` + InitialSync InitialSyncStrategy `yaml:"initialSync,omitempty" json:"initialSync,omitempty"` + InitialSyncCompareBy InitialSyncCompareBy `yaml:"initialSyncCompareBy,omitempty" json:"initialSyncCompareBy,omitempty"` + + DisableDownload *bool `yaml:"disableDownload,omitempty" json:"disableDownload,omitempty"` + DisableUpload *bool `yaml:"disableUpload,omitempty" json:"disableUpload,omitempty"` + + WaitInitialSync *bool `yaml:"waitInitialSync,omitempty" json:"waitInitialSync,omitempty"` + BandwidthLimits *BandwidthLimits `yaml:"bandwidthLimits,omitempty" json:"bandwidthLimits,omitempty"` // If greater zero, describes the amount of milliseconds to wait after each checked 100 files - ThrottleChangeDetection *int64 `yaml:"throttleChangeDetection,omitempty"` + ThrottleChangeDetection *int64 `yaml:"throttleChangeDetection,omitempty" json:"throttleChangeDetection,omitempty"` - OnUpload *SyncOnUpload `yaml:"onUpload,omitempty"` - OnDownload *SyncOnDownload `yaml:"onDownload,omitempty"` + OnUpload *SyncOnUpload `yaml:"onUpload,omitempty" json:"onUpload,omitempty"` + OnDownload *SyncOnDownload `yaml:"onDownload,omitempty" json:"onDownload,omitempty"` } // SyncOnUpload defines the struct for the command that should be executed when files / folders are uploaded @@ -556,41 +558,41 @@ type SyncOnUpload struct { // If true restart container will try to restart the container after a change has been made. Make sure that // images.*.injectRestartHelper is enabled for the container that should be restarted or the devspace-restart-helper // script is present in the container root folder. - RestartContainer bool `yaml:"restartContainer,omitempty"` + RestartContainer bool `yaml:"restartContainer,omitempty" json:"restartContainer,omitempty"` // Defines what commands should be executed on the container side if a change is uploaded and applied in the target // container - ExecRemote *SyncExecCommand `yaml:"execRemote,omitempty"` + ExecRemote *SyncExecCommand `yaml:"execRemote,omitempty" json:"execRemote,omitempty"` } // SyncOnDownload defines the struct for the command that should be executed when files / folders are downloaded type SyncOnDownload struct { - ExecLocal *SyncExecCommand `yaml:"execLocal,omitempty"` + ExecLocal *SyncExecCommand `yaml:"execLocal,omitempty" json:"execLocal,omitempty"` } // SyncExecCommand holds the configuration of commands that should be executed when files / folders are change type SyncExecCommand struct { - Command string `yaml:"command,omitempty"` - Args []string `yaml:"args,omitempty"` + Command string `yaml:"command,omitempty" json:"command,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` // OnFileChange is invoked after every file change. DevSpace will wait for the command to successfully finish // and then will continue to upload files & create folders - OnFileChange *SyncCommand `yaml:"onFileChange,omitempty"` + OnFileChange *SyncCommand `yaml:"onFileChange,omitempty" json:"onFileChange,omitempty"` // OnDirCreate is invoked after every directory that is created. DevSpace will wait for the command to successfully finish // and then will continue to upload files & create folders - OnDirCreate *SyncCommand `yaml:"onDirCreate,omitempty"` + OnDirCreate *SyncCommand `yaml:"onDirCreate,omitempty" json:"onDirCreate,omitempty"` // OnBatch executes the given command after a batch of changes has been processed. DevSpace will wait for the command to finish // and then will continue execution. This is useful for commands // that shouldn't be executed after every single change that may take a little bit longer like recompiling etc. - OnBatch *SyncCommand `yaml:"onBatch,omitempty"` + OnBatch *SyncCommand `yaml:"onBatch,omitempty" json:"onBatch,omitempty"` } // SyncCommand holds a command definition type SyncCommand struct { - Command string `yaml:"command,omitempty"` - Args []string `yaml:"args,omitempty"` + Command string `yaml:"command,omitempty" json:"command,omitempty"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` } // InitialSyncStrategy is the type of a initial sync strategy @@ -617,108 +619,111 @@ const ( // BandwidthLimits defines the struct for specifying the sync bandwidth limits type BandwidthLimits struct { - Download *int64 `yaml:"download,omitempty"` - Upload *int64 `yaml:"upload,omitempty"` + Download *int64 `yaml:"download,omitempty" json:"download,omitempty"` + Upload *int64 `yaml:"upload,omitempty" json:"upload,omitempty"` } // LogsConfig specifies the logs options for devspace dev type LogsConfig struct { - Disabled *bool `yaml:"disabled,omitempty"` - ShowLast *int `yaml:"showLast,omitempty"` - Images []string `yaml:"images,omitempty"` + Disabled *bool `yaml:"disabled,omitempty" json:"disabled,omitempty"` + ShowLast *int `yaml:"showLast,omitempty" json:"showLast,omitempty"` + Images []string `yaml:"images,omitempty" json:"images,omitempty"` } // AutoReloadConfig defines the struct for auto reloading devspace with additional paths type AutoReloadConfig struct { - Paths []string `yaml:"paths,omitempty"` - Deployments []string `yaml:"deployments,omitempty"` - Images []string `yaml:"images,omitempty"` + Paths []string `yaml:"paths,omitempty" json:"paths,omitempty"` + Deployments []string `yaml:"deployments,omitempty" json:"deployments,omitempty"` + Images []string `yaml:"images,omitempty" json:"images,omitempty"` } // InteractiveConfig defines the default interactive config type InteractiveConfig struct { - DefaultEnabled *bool `yaml:"defaultEnabled,omitempty"` - Images []*InteractiveImageConfig `yaml:"images,omitempty"` - Terminal *TerminalConfig `yaml:"terminal,omitempty"` + DefaultEnabled *bool `yaml:"defaultEnabled,omitempty" json:"defaultEnabled,omitempty"` + Images []*InteractiveImageConfig `yaml:"images,omitempty" json:"images,omitempty"` + Terminal *TerminalConfig `yaml:"terminal,omitempty" json:"terminal,omitempty"` } // InteractiveImageConfig describes the interactive mode options for an image type InteractiveImageConfig struct { - Name string `yaml:"name,omitempty"` - Entrypoint []string `yaml:"entrypoint,omitempty"` - Cmd []string `yaml:"cmd,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"` + Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"` } // TerminalConfig describes the terminal options type TerminalConfig struct { - ImageName string `yaml:"imageName,omitempty"` - LabelSelector map[string]string `yaml:"labelSelector,omitempty"` - ContainerName string `yaml:"containerName,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - Command []string `yaml:"command,omitempty"` + ImageName string `yaml:"imageName,omitempty" json:"imageName,omitempty"` + LabelSelector map[string]string `yaml:"labelSelector,omitempty" json:"labelSelector,omitempty"` + ContainerName string `yaml:"containerName,omitempty" json:"containerName,omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` + Command []string `yaml:"command,omitempty" json:"command,omitempty"` } // DependencyConfig defines the devspace dependency type DependencyConfig struct { - Name string `yaml:"name"` - Source *SourceConfig `yaml:"source"` - Profile string `yaml:"profile,omitempty"` - SkipBuild *bool `yaml:"skipBuild,omitempty"` - IgnoreDependencies *bool `yaml:"ignoreDependencies,omitempty"` - Namespace string `yaml:"namespace,omitempty"` + Name string `yaml:"name" json:"name"` + Source *SourceConfig `yaml:"source" json:"source"` + Profile string `yaml:"profile,omitempty" json:"profile,omitempty"` + SkipBuild *bool `yaml:"skipBuild,omitempty" json:"skipBuild,omitempty"` + IgnoreDependencies *bool `yaml:"ignoreDependencies,omitempty" json:"ignoreDependencies,omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` } // SourceConfig defines the dependency source type SourceConfig struct { - Git string `yaml:"git,omitempty"` - CloneArgs []string `yaml:"cloneArgs,omitempty"` - DisableShallow bool `yaml:"disableShallow,omitempty"` - SubPath string `yaml:"subPath,omitempty"` - Branch string `yaml:"branch,omitempty"` - Tag string `yaml:"tag,omitempty"` - Revision string `yaml:"revision,omitempty"` - ConfigName string `yaml:"configName,omitempty"` + Git string `yaml:"git,omitempty" json:"git,omitempty"` + CloneArgs []string `yaml:"cloneArgs,omitempty" json:"cloneArgs,omitempty"` + DisableShallow bool `yaml:"disableShallow,omitempty" json:"disableShallow,omitempty"` + SubPath string `yaml:"subPath,omitempty" json:"subPath,omitempty"` + Branch string `yaml:"branch,omitempty" json:"branch,omitempty"` + Tag string `yaml:"tag,omitempty" json:"tag,omitempty"` + Revision string `yaml:"revision,omitempty" json:"revision,omitempty"` + ConfigName string `yaml:"configName,omitempty" json:"configName,omitempty"` - Path string `yaml:"path,omitempty"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` } // HookConfig defines a hook type HookConfig struct { - Command string `yaml:"command"` - Args []string `yaml:"args,omitempty"` + Command string `yaml:"command" json:"command"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + OperatingSystem string `yaml:"os,omitempty" json:"os,omitempty"` - When *HookWhenConfig `yaml:"when,omitempty"` + When *HookWhenConfig `yaml:"when,omitempty" json:"when,omitempty"` } // HookWhenConfig defines when the hook should be executed type HookWhenConfig struct { - Before *HookWhenAtConfig `yaml:"before,omitempty"` - After *HookWhenAtConfig `yaml:"after,omitempty"` + Before *HookWhenAtConfig `yaml:"before,omitempty" json:"before,omitempty"` + After *HookWhenAtConfig `yaml:"after,omitempty" json:"after,omitempty"` } // HookWhenAtConfig defines at which stage the hook should be executed type HookWhenAtConfig struct { - Images string `yaml:"images,omitempty"` - Deployments string `yaml:"deployments,omitempty"` + Images string `yaml:"images,omitempty" json:"images,omitempty"` + Deployments string `yaml:"deployments,omitempty" json:"deployments,omitempty"` + Dependencies string `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` + PullSecrets string `yaml:"pullSecrets,omitempty" json:"pullSecrets,omitempty"` } // CommandConfig defines the command specification type CommandConfig struct { - Name string `yaml:"name"` - Command string `yaml:"command"` - Description string `yaml:"description"` + Name string `yaml:"name" json:"name"` + Command string `yaml:"command" json:"command"` + Description string `yaml:"description" json:"description"` } // Variable describes the var definition type Variable struct { - Name string `yaml:"name"` - Question string `yaml:"question,omitempty"` - Options []string `yaml:"options,omitempty"` - Password bool `yaml:"password,omitempty"` - ValidationPattern string `yaml:"validationPattern,omitempty"` - ValidationMessage string `yaml:"validationMessage,omitempty"` - Default interface{} `yaml:"default,omitempty"` - Source VariableSource `yaml:"source,omitempty"` + Name string `yaml:"name" json:"name"` + Question string `yaml:"question,omitempty" json:"question,omitempty"` + Options []string `yaml:"options,omitempty" json:"options,omitempty"` + Password bool `yaml:"password,omitempty" json:"password,omitempty"` + ValidationPattern string `yaml:"validationPattern,omitempty" json:"validationPattern,omitempty"` + ValidationMessage string `yaml:"validationMessage,omitempty" json:"validationMessage,omitempty"` + Default interface{} `yaml:"default,omitempty" json:"default,omitempty"` + Source VariableSource `yaml:"source,omitempty" json:"source,omitempty"` } // VariableSource is type of a variable source @@ -735,32 +740,59 @@ const ( // ProfileConfig defines a profile config type ProfileConfig struct { - Name string `yaml:"name"` - Parent string `yaml:"parent,omitempty"` - Parents []*ProfileParent `yaml:"parents,omitempty"` - Patches []*PatchConfig `yaml:"patches,omitempty"` - Replace *ReplaceConfig `yaml:"replace,omitempty"` + Name string `yaml:"name" json:"name"` + Parent string `yaml:"parent,omitempty" json:"parent,omitempty"` + Parents []*ProfileParent `yaml:"parents,omitempty" json:"parents,omitempty"` + Patches []*PatchConfig `yaml:"patches,omitempty" json:"patches,omitempty"` + Replace *ReplaceConfig `yaml:"replace,omitempty" json:"replace,omitempty"` + Merge map[interface{}]interface{} `yaml:"merge,omitempty" json:"merge,omitempty"` + StrategicMerge map[interface{}]interface{} `yaml:"strategicMerge,omitempty" json:"strategicMerge,omitempty"` } // ProfileParent defines where to load the profile from type ProfileParent struct { - Source *SourceConfig `yaml:"source,omitempty"` - Profile string `yaml:"profile"` + Source *SourceConfig `yaml:"source,omitempty" json:"source,omitempty"` + Profile string `yaml:"profile" json:"profile"` } // PatchConfig describes a config patch and how it should be applied type PatchConfig struct { - Operation string `yaml:"op"` - Path string `yaml:"path"` - Value interface{} `yaml:"value,omitempty"` - From string `yaml:"from,omitempty"` + Operation string `yaml:"op" json:"op"` + Path string `yaml:"path" json:"path"` + Value interface{} `yaml:"value,omitempty" json:"value,omitempty"` + From string `yaml:"from,omitempty" json:"from,omitempty"` } // ReplaceConfig defines a replace config that can override certain parts of the config completely type ReplaceConfig struct { - Images map[string]*ImageConfig `yaml:"images,omitempty"` - Deployments []*DeploymentConfig `yaml:"deployments,omitempty"` - Dev *DevConfig `yaml:"dev,omitempty"` - Dependencies []*DependencyConfig `yaml:"dependencies,omitempty"` - Hooks []*HookConfig `yaml:"hooks,omitempty"` + Images map[string]*ImageConfig `yaml:"images,omitempty" json:"images,omitempty"` + Deployments []*DeploymentConfig `yaml:"deployments,omitempty" json:"deployments,omitempty"` + Dev *DevConfig `yaml:"dev,omitempty" json:"dev,omitempty"` + Dependencies []*DependencyConfig `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` + Hooks []*HookConfig `yaml:"hooks,omitempty" json:"hooks,omitempty"` + PullSecrets []*PullSecretConfig `yaml:"pullSecrets,omitempty" json:"pullSecrets,omitempty"` +} + +// PullSecretConfig defines a pull secret that should be created by DevSpace +type PullSecretConfig struct { + // The registry to create the image pull secret for. + // e.g. gcr.io + Registry string `yaml:"registry" json:"registry"` + + // The username of the registry. If this is empty, devspace will try + // to receive the auth data from the local docker + Username string `yaml:"username,omitempty" json:"username,omitempty"` + + // The password to use for the registry. If this is empty, devspace will + // try to receive the auth data from the local docker + Password string `yaml:"password,omitempty" json:"password,omitempty"` + + // The optional email to use + Email string `yaml:"email,omitempty" json:"email,omitempty"` + + // The secret to create + Secret string `yaml:"secret,omitempty" json:"secret,omitempty"` + + // The service account to add the secret to + ServiceAccounts []string `yaml:"serviceAccounts,omitempty" json:"serviceAccounts,omitempty"` } diff --git a/pkg/devspace/configure/image.go b/pkg/devspace/configure/image.go index 433bca2106..10765f8152 100644 --- a/pkg/devspace/configure/image.go +++ b/pkg/devspace/configure/image.go @@ -11,7 +11,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" v1 "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/devspace/docker" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/util/ptr" "github.com/devspace-cloud/devspace/pkg/util/survey" "github.com/pkg/errors" @@ -112,7 +112,7 @@ func (m *manager) newImageConfigFromDockerfile(imageName, dockerfile, context st DefaultValue: dockerUsername + "/" + imageName, ValidationMessage: "Please enter a valid image name for Docker Hub (e.g. myregistry.com/user/repository | allowed charaters: /, a-z, 0-9)", ValidationFunc: func(name string) error { - _, err := registry.GetStrippedDockerImageName(name) + _, err := pullsecrets.GetStrippedDockerImageName(name) return err }, }) @@ -120,7 +120,7 @@ func (m *manager) newImageConfigFromDockerfile(imageName, dockerfile, context st return nil, err } - imageName, _ = registry.GetStrippedDockerImageName(imageName) + imageName, _ = pullsecrets.GetStrippedDockerImageName(imageName) } else if regexp.MustCompile("^(.+\\.)?gcr.io$").Match([]byte(registryURL)) { // Is google registry? project, err := exec.Command("gcloud", "config", "get-value", "project").Output() gcloudProject := "myGCloudProject" @@ -134,7 +134,7 @@ func (m *manager) newImageConfigFromDockerfile(imageName, dockerfile, context st DefaultValue: registryURL + "/" + gcloudProject + "/" + imageName, ValidationMessage: "Please enter a valid Docker image name (e.g. myregistry.com/user/repository | allowed charaters: /, a-z, 0-9)", ValidationFunc: func(name string) error { - _, err := registry.GetStrippedDockerImageName(name) + _, err := pullsecrets.GetStrippedDockerImageName(name) return err }, }) @@ -142,7 +142,7 @@ func (m *manager) newImageConfigFromDockerfile(imageName, dockerfile, context st return nil, err } - imageName, _ = registry.GetStrippedDockerImageName(imageName) + imageName, _ = pullsecrets.GetStrippedDockerImageName(imageName) } else { if dockerUsername == "" { dockerUsername = "myuser" @@ -153,7 +153,7 @@ func (m *manager) newImageConfigFromDockerfile(imageName, dockerfile, context st DefaultValue: registryURL + "/" + dockerUsername + "/" + imageName, ValidationMessage: "Please enter a valid docker image name (e.g. myregistry.com/user/repository)", ValidationFunc: func(name string) error { - _, err := registry.GetStrippedDockerImageName(name) + _, err := pullsecrets.GetStrippedDockerImageName(name) return err }, }) @@ -161,7 +161,7 @@ func (m *manager) newImageConfigFromDockerfile(imageName, dockerfile, context st return nil, err } - imageName, _ = registry.GetStrippedDockerImageName(imageName) + imageName, _ = pullsecrets.GetStrippedDockerImageName(imageName) } targets, err := helper.GetDockerfileTargets(dockerfile) diff --git a/pkg/devspace/dependency/dependency.go b/pkg/devspace/dependency/dependency.go index 45841d2fd7..880ce140b8 100644 --- a/pkg/devspace/dependency/dependency.go +++ b/pkg/devspace/dependency/dependency.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "github.com/devspace-cloud/devspace/pkg/devspace/command" + "github.com/devspace-cloud/devspace/pkg/devspace/hook" "github.com/devspace-cloud/devspace/pkg/util/exit" "mvdan.cc/sh/v3/interp" "os" @@ -14,7 +15,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/devspace/deploy" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/util/hash" "github.com/devspace-cloud/devspace/pkg/util/log" @@ -34,9 +35,10 @@ type Manager interface { } type manager struct { - config *latest.Config - log log.Logger - resolver ResolverInterface + config *latest.Config + log log.Logger + resolver ResolverInterface + hookExecuter hook.Executer } // NewManager creates a new instance of the interface Manager @@ -47,9 +49,10 @@ func NewManager(config *latest.Config, cache *generated.Config, client kubectl.C } return &manager{ - config: config, - log: logger, - resolver: resolver, + config: config, + log: logger, + resolver: resolver, + hookExecuter: hook.NewExecuter(config), }, nil } @@ -151,15 +154,31 @@ type DeployOptions struct { ForceDeployDependencies bool SkipBuild bool ForceBuild bool + SkipDeploy bool ForceDeploy bool Verbose bool } // DeployAll will deploy all dependencies if there are any func (m *manager) DeployAll(options DeployOptions) error { - return m.handleDependencies(options.Dependencies, false, options.UpdateDependencies, false, options.Verbose, "Deploy", func(dependency *Dependency, log log.Logger) error { - return dependency.Deploy(options.SkipPush, options.ForceDeployDependencies, options.SkipBuild, options.ForceBuild, options.ForceDeploy, log) + err := m.hookExecuter.Execute(hook.Before, hook.StageDependencies, hook.All, m.log) + if err != nil { + return err + } + + err = m.handleDependencies(options.Dependencies, false, options.UpdateDependencies, false, options.Verbose, "Deploy", func(dependency *Dependency, log log.Logger) error { + return dependency.Deploy(options.SkipPush, options.ForceDeployDependencies, options.SkipBuild, options.ForceBuild, options.SkipDeploy, options.ForceDeploy, log) }) + if err != nil { + return err + } + + err = m.hookExecuter.Execute(hook.After, hook.StageDependencies, hook.All, m.log) + if err != nil { + return err + } + + return nil } // PurgeOptions has all options for purging all dependencies @@ -269,7 +288,6 @@ func (m *manager) handleDependencies(filterDependencies []string, reverse, updat } } m.log.StopWait() - if silent == false { if executed > 0 { m.log.Donef("Successfully processed %d dependencies", executed) @@ -293,7 +311,7 @@ type Dependency struct { DependencyCache *generated.Config kubeClient kubectl.Client - registryClient registry.Client + registryClient pullsecrets.Client buildController build.Controller deployController deploy.Controller generatedSaver generated.ConfigLoader @@ -323,7 +341,7 @@ func (d *Dependency) Build(skipPush, forceDependencies, forceBuild bool, log log } // Deploy deploys the dependency if necessary -func (d *Dependency) Deploy(skipPush, forceDependencies, skipBuild, forceBuild, forceDeploy bool, log log.Logger) error { +func (d *Dependency) Deploy(skipPush, forceDependencies, skipBuild, forceBuild, skipDeploy, forceDeploy bool, log log.Logger) error { // Switch current working directory currentWorkingDirectory, err := d.prepare(forceDependencies) if err != nil { @@ -354,12 +372,14 @@ func (d *Dependency) Deploy(skipPush, forceDependencies, skipBuild, forceBuild, } // Deploy all defined deployments - err = d.deployController.Deploy(&deploy.Options{ - ForceDeploy: forceDeploy, - BuiltImages: builtImages, - }, log) - if err != nil { - return err + if skipDeploy == false { + err = d.deployController.Deploy(&deploy.Options{ + ForceDeploy: forceDeploy, + BuiltImages: builtImages, + }, log) + if err != nil { + return err + } } // Save Config diff --git a/pkg/devspace/dependency/dependency_test.go b/pkg/devspace/dependency/dependency_test.go index f45feb33e7..184a98d4e0 100644 --- a/pkg/devspace/dependency/dependency_test.go +++ b/pkg/devspace/dependency/dependency_test.go @@ -1,6 +1,7 @@ package dependency import ( + "github.com/devspace-cloud/devspace/pkg/devspace/hook" "io/ioutil" "os" "path/filepath" @@ -13,7 +14,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" fakedeploy "github.com/devspace-cloud/devspace/pkg/devspace/deploy/testing" fakekube "github.com/devspace-cloud/devspace/pkg/devspace/kubectl/testing" - fakeregistry "github.com/devspace-cloud/devspace/pkg/devspace/registry/testing" + fakeregistry "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets/testing" "github.com/devspace-cloud/devspace/pkg/util/fsutil" "github.com/devspace-cloud/devspace/pkg/util/hash" "github.com/devspace-cloud/devspace/pkg/util/log" @@ -333,6 +334,9 @@ func TestDeployAll(t *testing.T) { resolver: &fakeResolver{ resolvedDependencies: testCase.resolvedDependencies, }, + hookExecuter: hook.NewExecuter(&latest.Config{ + Dependencies: testCase.dependencyTasks, + }), } err = manager.DeployAll(testCase.options) @@ -579,6 +583,7 @@ type deployTestCase struct { forceDependencies bool skipBuild bool forceBuild bool + skipDeploy bool forceDeploy bool expectedErr string @@ -672,7 +677,7 @@ func TestDeploy(t *testing.T) { }).Resolve(false) dependency := dependencies[0] - err = dependency.Deploy(testCase.skipPush, testCase.forceDependencies, testCase.skipBuild, testCase.forceBuild, testCase.forceDeploy, log.Discard) + err = dependency.Deploy(testCase.skipPush, testCase.forceDependencies, testCase.skipBuild, testCase.forceBuild, testCase.skipDeploy, testCase.forceDeploy, log.Discard) if testCase.expectedErr == "" { assert.NilError(t, err, "Error purging all in testCase %s", testCase.name) diff --git a/pkg/devspace/dependency/resolver.go b/pkg/devspace/dependency/resolver.go index c1a6eb20f7..f2adb59168 100644 --- a/pkg/devspace/dependency/resolver.go +++ b/pkg/devspace/dependency/resolver.go @@ -11,7 +11,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/deploy" "github.com/devspace-cloud/devspace/pkg/devspace/docker" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/util/git" "github.com/devspace-cloud/devspace/pkg/util/kubeconfig" "github.com/devspace-cloud/devspace/pkg/util/log" @@ -251,7 +251,7 @@ func (r *resolver) resolveDependency(basePath string, dependency *latest.Depende } // Create registry client for pull secrets - registryClient := registry.NewClient(dConfig, client, dockerClient, r.log) + registryClient := pullsecrets.NewClient(dConfig, client, dockerClient, r.log) return &Dependency{ ID: ID, diff --git a/pkg/devspace/deploy/deployer/util/replace.go b/pkg/devspace/deploy/deployer/util/replace.go index e8b238ab8b..bfa4c5759d 100644 --- a/pkg/devspace/deploy/deployer/util/replace.go +++ b/pkg/devspace/deploy/deployer/util/replace.go @@ -4,7 +4,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/generated" "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/devspace/deploy/deployer/kubectl/walk" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" ) func replaceImageNames(cache *generated.CacheConfig, imagesConf map[string]*latest.ImageConfig, builtImages map[string]string, keys map[string]bool, action func(walk.MatchFn, walk.ReplaceFn)) bool { @@ -30,7 +30,7 @@ func replaceImageNames(cache *generated.CacheConfig, imagesConf map[string]*late } // Strip tag from image - image, err := registry.GetStrippedDockerImageName(value) + image, err := pullsecrets.GetStrippedDockerImageName(value) if err != nil { return false } @@ -52,7 +52,7 @@ func replaceImageNames(cache *generated.CacheConfig, imagesConf map[string]*late } replace := func(path, value string) (interface{}, error) { - image, err := registry.GetStrippedDockerImageName(value) + image, err := pullsecrets.GetStrippedDockerImageName(value) if err != nil { return false, nil } diff --git a/pkg/devspace/hook/hook.go b/pkg/devspace/hook/hook.go index 9e48857294..2b039004d8 100644 --- a/pkg/devspace/hook/hook.go +++ b/pkg/devspace/hook/hook.go @@ -3,6 +3,7 @@ package hook import ( "fmt" "io" + "runtime" "strings" "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" @@ -48,6 +49,10 @@ const ( StageImages Stage = iota // StageDeployments is the deploying stage StageDeployments + // StageDependencies is the dependency stage + StageDependencies + // StagePullSecrets is the pull secrets stage + StagePullSecrets ) // All is used to tell devspace to execute a hook before or after all images, deployments @@ -70,12 +75,20 @@ func (e *executer) Execute(when When, stage Stage, which string, log logpkg.Logg hooksToExecute = append(hooksToExecute, hook) } else if stage == StageImages && hook.When.Before.Images != "" && strings.TrimSpace(hook.When.Before.Images) == strings.TrimSpace(which) { hooksToExecute = append(hooksToExecute, hook) + } else if stage == StageDependencies && hook.When.Before.Dependencies != "" && strings.TrimSpace(hook.When.Before.Dependencies) == strings.TrimSpace(which) { + hooksToExecute = append(hooksToExecute, hook) + } else if stage == StagePullSecrets && hook.When.Before.PullSecrets != "" && strings.TrimSpace(hook.When.Before.PullSecrets) == strings.TrimSpace(which) { + hooksToExecute = append(hooksToExecute, hook) } } else if when == After && hook.When.After != nil { if stage == StageDeployments && hook.When.After.Deployments != "" && strings.TrimSpace(hook.When.After.Deployments) == strings.TrimSpace(which) { hooksToExecute = append(hooksToExecute, hook) } else if stage == StageImages && hook.When.After.Images != "" && strings.TrimSpace(hook.When.After.Images) == strings.TrimSpace(which) { hooksToExecute = append(hooksToExecute, hook) + } else if stage == StageDependencies && hook.When.After.Dependencies != "" && strings.TrimSpace(hook.When.After.Dependencies) == strings.TrimSpace(which) { + hooksToExecute = append(hooksToExecute, hook) + } else if stage == StagePullSecrets && hook.When.After.PullSecrets != "" && strings.TrimSpace(hook.When.After.PullSecrets) == strings.TrimSpace(which) { + hooksToExecute = append(hooksToExecute, hook) } } } @@ -83,6 +96,22 @@ func (e *executer) Execute(when When, stage Stage, which string, log logpkg.Logg // Execute hooks for _, hook := range hooksToExecute { + // if the operating system is set and the current is not specified + // we skip the hook + if hook.OperatingSystem != "" { + found := false + oss := strings.Split(hook.OperatingSystem, ",") + for _, os := range oss { + if strings.TrimSpace(os) == runtime.GOOS { + found = true + break + } + } + if found == false { + continue + } + } + // Build arguments args := []string{} if hook.Args != nil { diff --git a/pkg/devspace/plugin/plugin.go b/pkg/devspace/plugin/plugin.go index 730d8299d7..204ebbf8de 100644 --- a/pkg/devspace/plugin/plugin.go +++ b/pkg/devspace/plugin/plugin.go @@ -1,16 +1,23 @@ package plugin import ( + "bytes" "encoding/base32" + "encoding/json" "fmt" "github.com/blang/semver" "github.com/devspace-cloud/devspace/pkg/devspace/config/constants" + "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/util/exit" "github.com/devspace-cloud/devspace/pkg/util/log" "github.com/ghodss/yaml" + "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" + yaml2 "gopkg.in/yaml.v2" + "io" "io/ioutil" "os" @@ -28,6 +35,17 @@ var PluginBinary = "binary" const PluginCommandAnnotation = "devspace.sh/is-plugin" +const ( + KubeContextFlagEnv = "DEVSPACE_PLUGIN_KUBE_CONTEXT_FLAG" + KubeNamespaceFlagEnv = "DEVSPACE_PLUGIN_KUBE_NAMESPACE_FLAG" + ConfigEnv = "DEVSPACE_PLUGIN_CONFIG" + OsArgsEnv = "DEVSPACE_PLUGIN_OS_ARGS" + CommandEnv = "DEVSPACE_PLUGIN_COMMAND" + CommandLineEnv = "DEVSPACE_PLUGIN_COMMAND_LINE" + CommandFlagsEnv = "DEVSPACE_PLUGIN_COMMAND_FLAGS" + CommandArgsEnv = "DEVSPACE_PLUGIN_COMMAND_ARGS" +) + func init() { if runtime.GOOS == "windows" { PluginBinary += ".exe" @@ -35,8 +53,9 @@ func init() { } type Interface interface { - Add(path, version string) error - Update(name, version string) error + Add(path, version string) (*Metadata, error) + GetByName(name string) (string, *Metadata, error) + Update(name, version string) (*Metadata, error) Remove(name string) error List() ([]Metadata, error) @@ -64,21 +83,21 @@ func (c *client) PluginFolder() (string, error) { return filepath.Join(dir, constants.DefaultHomeDevSpaceFolder, PluginFolder), nil } -func (c *client) Add(path, version string) error { +func (c *client) Add(path, version string) (*Metadata, error) { metadata, err := c.Get(path) if err != nil { - return err + return nil, err } else if metadata != nil { - return fmt.Errorf("plugin %s already exists", path) + return nil, fmt.Errorf("plugin %s already exists", path) } return c.install(path, version) } -func (c *client) install(path, version string) error { +func (c *client) install(path, version string) (*Metadata, error) { metadata, err := c.installer.DownloadMetadata(path, version) if err != nil { - return errors.Wrap(err, "download metadata") + return nil, errors.Wrap(err, "download metadata") } // find binary for system @@ -92,68 +111,69 @@ func (c *client) install(path, version string) error { } } if found == false { - return fmt.Errorf("plugin %s does not support %s/%s", metadata.Name, runtime.GOOS, runtime.GOARCH) + return nil, fmt.Errorf("plugin %s does not support %s/%s", metadata.Name, runtime.GOOS, runtime.GOARCH) } pluginFolder, err := c.PluginFolder() if err != nil { - return err + return nil, err } pluginFolder = filepath.Join(pluginFolder, Encode(path)) err = os.MkdirAll(pluginFolder, 0755) if err != nil { - return err + return nil, err } out, err := yaml.Marshal(metadata) if err != nil { - return err + return nil, err } err = ioutil.WriteFile(filepath.Join(pluginFolder, pluginYaml), out, 0666) if err != nil { - return err + return nil, err } outBinaryPath := filepath.Join(pluginFolder, PluginBinary) err = c.installer.DownloadBinary(path, version, binaryPath, outBinaryPath) if err != nil { - return errors.Wrap(err, "download plugin binary") + return nil, errors.Wrap(err, "download plugin binary") } // make the file executable _ = os.Chmod(outBinaryPath, 0755) - return nil + metadata.PluginFolder = pluginFolder + return metadata, nil } -func (c *client) Update(name, version string) error { +func (c *client) Update(name, version string) (*Metadata, error) { path, metadata, err := c.GetByName(name) if err != nil { - return err + return nil, err } else if metadata == nil { - return fmt.Errorf("couldn't find plugin %s", name) + return nil, fmt.Errorf("couldn't find plugin %s", name) } oldVersion, err := c.parseVersion(metadata.Version) if err != nil { - return errors.Wrap(err, "parse old version") + return nil, errors.Wrap(err, "parse old version") } newMetadata, err := c.installer.DownloadMetadata(path, version) if err != nil { - return err + return nil, err } newVersion, err := c.parseVersion(newMetadata.Version) if err != nil { - return errors.Wrap(err, "parse new version") + return nil, errors.Wrap(err, "parse new version") } if oldVersion.EQ(newVersion) { - return fmt.Errorf("no update for plugin found") + return nil, fmt.Errorf("no update for plugin found") } else if oldVersion.GT(newVersion) { - return fmt.Errorf("new version is older than existing version") + return nil, fmt.Errorf("new version is older than existing version") } c.log.Infof("Updating plugin %s to version %s", name, newMetadata.Version) @@ -265,6 +285,7 @@ func (c *client) GetByName(name string) (string, *Metadata, error) { return "", nil, errors.Wrap(err, "decode plugin path") } + metadata.PluginFolder = filepath.Join(pluginFolder, plugin.Name()) return string(decoded), &metadata, nil } } @@ -337,15 +358,73 @@ func AddPluginCommands(base *cobra.Command, plugins []Metadata, subCommand strin } } -func ExecutePluginHook(plugins []Metadata, event, kubeContext, namespace string) error { +func ExecutePluginHook(plugins []Metadata, cobraCmd *cobra.Command, args []string, event, kubeContext, namespace string, config *latest.Config) error { + configStr := "" + if config != nil { + configBytes, err := yaml2.Marshal(config) + if err != nil { + return err + } + + configStr = string(configBytes) + } + + osArgsBytes, err := json.Marshal(os.Args) + if err != nil { + return err + } + + // build environment variables + env := map[string]string{ + CommandEnv: cobraCmd.Use, + CommandLineEnv: cobraCmd.UseLine(), + OsArgsEnv: string(osArgsBytes), + } + if kubeContext != "" { + env[KubeContextFlagEnv] = kubeContext + } + if namespace != "" { + env[KubeNamespaceFlagEnv] = namespace + } + if configStr != "" { + env[ConfigEnv] = configStr + } + + // Flags + flags := []string{} + cobraCmd.Flags().Visit(func(f *pflag.Flag) { + flags = append(flags, "--"+f.Name) + flags = append(flags, f.Value.String()) + }) + if len(flags) > 0 { + flagsStr, err := json.Marshal(flags) + if err != nil { + return err + } + + env[CommandFlagsEnv] = string(flagsStr) + } + + // Args + if len(args) > 0 { + argsStr, err := json.Marshal(args) + if err != nil { + return err + } + if string(argsStr) != "" { + env[CommandArgsEnv] = string(argsStr) + } + } + for _, plugin := range plugins { pluginFolder := plugin.PluginFolder for _, pluginHook := range plugin.Hooks { - if pluginHook.Event == event { - err := CallPluginExecutable(filepath.Join(pluginFolder, PluginBinary), pluginHook.BaseArgs, map[string]string{ - "DEVSPACE_PLUGIN_KUBE_CONTEXT_FLAG": kubeContext, - "DEVSPACE_PLUGIN_KUBE_NAMESPACE_FLAG": namespace, - }, os.Stdout) + if strings.TrimSpace(pluginHook.Event) == event { + if pluginHook.Background { + err = CallPluginExecutableInBackground(filepath.Join(pluginFolder, PluginBinary), pluginHook.BaseArgs, env) + } else { + err = CallPluginExecutable(filepath.Join(pluginFolder, PluginBinary), pluginHook.BaseArgs, env, os.Stdout) + } if err != nil { return err } @@ -356,6 +435,35 @@ func ExecutePluginHook(plugins []Metadata, event, kubeContext, namespace string) return nil } +func CallPluginExecutableInBackground(main string, argv []string, extraEnvVars map[string]string) error { + env := os.Environ() + for k, v := range extraEnvVars { + env = append(env, k+"="+v) + } + + stderrOut := &bytes.Buffer{} + prog := exec.Command(main, argv...) + prog.Env = env + prog.Stderr = stderrOut + if err := prog.Start(); err != nil { + if strings.Index(err.Error(), "no such file or directory") != -1 { + return fmt.Errorf("the plugin's binary was not found (%v). Please uninstall and reinstall the plugin and make sure there are no other conflicting plugins installed (run 'devspace list plugins' to see all installed plugins)", err) + } + + return err + } + + go func() { + err := prog.Wait() + if err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + os.Stderr.Write([]byte(fmt.Sprintf("Hook %s failed (code: %d): %s", main+" "+strings.Join(argv, " "), eerr.ExitCode(), stderrOut.String()))) + } + } + }() + return nil +} + // This function is used to setup the environment for the plugin and then // call the executable specified by the parameter 'main' func CallPluginExecutable(main string, argv []string, extraEnvVars map[string]string, out io.Writer) error { diff --git a/pkg/devspace/plugin/schema.go b/pkg/devspace/plugin/schema.go index 7ad2bb2158..827bf0d9a6 100644 --- a/pkg/devspace/plugin/schema.go +++ b/pkg/devspace/plugin/schema.go @@ -21,7 +21,7 @@ type Metadata struct { Vars []Variable `json:"vars,omitempty"` // Hooks are commands that will be executed at specific events - Hooks []Hook `json:"hook,omitempty"` + Hooks []Hook `json:"hooks,omitempty"` // This will be filled after parsing the metadata PluginFolder string `json:"pluginFolder,omitempty"` @@ -31,13 +31,17 @@ type Hook struct { // Event is the name of the event when to execute this hook Event string `json:"event"` + // Background specifies if the given command should be executed in the background + Background bool `json:"background"` + // BaseArgs that will be prepended to all supplied user flags for this plugin command BaseArgs []string `json:"baseArgs,omitempty"` } type Binary struct { // The current OS - OS string `json:"os"` + OS string `json:"os"` + // The current Arch Arch string `json:"arch"` @@ -68,4 +72,4 @@ type Variable struct { // BaseArgs that will be prepended to all supplied user flags for this plugin command BaseArgs []string `json:"baseArgs,omitempty"` -} \ No newline at end of file +} diff --git a/pkg/devspace/registry/client.go b/pkg/devspace/pullsecrets/client.go similarity index 84% rename from pkg/devspace/registry/client.go rename to pkg/devspace/pullsecrets/client.go index 54d1c6e230..8e3ffc5cd5 100644 --- a/pkg/devspace/registry/client.go +++ b/pkg/devspace/pullsecrets/client.go @@ -1,8 +1,9 @@ -package registry +package pullsecrets import ( "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" "github.com/devspace-cloud/devspace/pkg/devspace/docker" + "github.com/devspace-cloud/devspace/pkg/devspace/hook" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" "github.com/devspace-cloud/devspace/pkg/util/log" ) @@ -19,6 +20,7 @@ func NewClient(config *latest.Config, kubeClient kubectl.Client, dockerClient do config: config, kubeClient: kubeClient, dockerClient: dockerClient, + hookExecuter: hook.NewExecuter(config), log: log, } } @@ -27,5 +29,6 @@ type client struct { config *latest.Config kubeClient kubectl.Client dockerClient docker.Client + hookExecuter hook.Executer log log.Logger } diff --git a/pkg/devspace/pullsecrets/init.go b/pkg/devspace/pullsecrets/init.go new file mode 100644 index 0000000000..0c4fc6dcf0 --- /dev/null +++ b/pkg/devspace/pullsecrets/init.go @@ -0,0 +1,200 @@ +package pullsecrets + +import ( + "context" + "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" + "github.com/devspace-cloud/devspace/pkg/devspace/hook" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + "time" + + "github.com/pkg/errors" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CreatePullSecrets creates the image pull secrets +func (r *client) CreatePullSecrets() error { + createPullSecrets := []*latest.PullSecretConfig{} + + // execute before pull secrets hooks + err := r.hookExecuter.Execute(hook.Before, hook.StagePullSecrets, hook.All, r.log) + if err != nil { + return err + } + + // gather pull secrets from pullSecrets + for _, pullSecret := range r.config.PullSecrets { + createPullSecrets = append(createPullSecrets, pullSecret) + } + + // gather pull secrets from images + for _, imageConf := range r.config.Images { + if imageConf.CreatePullSecret == nil || *imageConf.CreatePullSecret == true { + registryURL, err := GetRegistryFromImageName(imageConf.Image) + if err != nil { + return err + } + + if contains(registryURL, createPullSecrets) == false { + createPullSecrets = append(createPullSecrets, &latest.PullSecretConfig{ + Registry: registryURL, + }) + } + } + } + + // create pull secrets + for _, pullSecretConf := range createPullSecrets { + displayRegistryURL := pullSecretConf.Registry + if displayRegistryURL == "" { + displayRegistryURL = "hub.docker.com" + } + if pullSecretConf.Secret == "" { + pullSecretConf.Secret = GetRegistryAuthSecretName(pullSecretConf.Registry) + } + + r.log.StartWait("Creating image pull secret for registry: " + displayRegistryURL) + err := r.createPullSecretForRegistry(pullSecretConf) + r.log.StopWait() + if err != nil { + return errors.Errorf("failed to create pull secret for registry: %v", err) + } + + if len(pullSecretConf.ServiceAccounts) > 0 { + for _, serviceAccount := range pullSecretConf.ServiceAccounts { + err = r.addPullSecretsToServiceAccount(pullSecretConf.Secret, serviceAccount) + if err != nil { + return errors.Wrap(err, "add pull secrets to service account") + } + } + } else { + err = r.addPullSecretsToServiceAccount(pullSecretConf.Secret, "default") + if err != nil { + return errors.Wrap(err, "add pull secrets to service account") + } + } + } + + // execute after pull secrets hooks + err = r.hookExecuter.Execute(hook.After, hook.StagePullSecrets, hook.All, r.log) + if err != nil { + return err + } + + return nil +} + +func contains(registryURL string, pullSecrets []*latest.PullSecretConfig) bool { + for _, v := range pullSecrets { + if v.Registry == registryURL { + return true + } + } + return false +} + +func (r *client) addPullSecretsToServiceAccount(pullSecretName string, serviceAccount string) error { + if serviceAccount == "" { + serviceAccount = "default" + } + + err := wait.PollImmediate(time.Second, time.Second*30, func() (bool, error) { + // Get default service account + sa, err := r.kubeClient.KubeClient().CoreV1().ServiceAccounts(r.kubeClient.Namespace()).Get(context.TODO(), serviceAccount, metav1.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + return false, nil + } + + r.log.Errorf("Couldn't retrieve service account '%s' in namespace '%s': %v", serviceAccount, r.kubeClient.Namespace(), err) + return false, err + } + + // Check if all pull secrets are there + found := false + for _, pullSecret := range sa.ImagePullSecrets { + if pullSecret.Name == pullSecretName { + found = true + break + } + } + if found == false { + sa.ImagePullSecrets = append(sa.ImagePullSecrets, v1.LocalObjectReference{Name: pullSecretName}) + _, err := r.kubeClient.KubeClient().CoreV1().ServiceAccounts(r.kubeClient.Namespace()).Update(context.TODO(), sa, metav1.UpdateOptions{}) + if err != nil { + if kerrors.IsConflict(err) { + return false, nil + } + + return false, errors.Wrap(err, "update service account") + } + } + + return true, nil + }) + if err != nil { + return errors.Wrap(err, "add pull secret to service account") + } + + return nil +} + +func (r *client) createPullSecretForRegistry(pullSecret *latest.PullSecretConfig) error { + username := pullSecret.Username + password := pullSecret.Password + if username == "" && password == "" && r.dockerClient != nil { + authConfig, _ := r.dockerClient.GetAuthConfig(pullSecret.Registry, true) + if authConfig != nil { + username = authConfig.Username + password = authConfig.Password + } + } + + email := pullSecret.Email + if email == "" { + email = "noreply@devspace.cloud" + } + + if username != "" && password != "" { + defaultNamespace := r.kubeClient.Namespace() + err := r.CreatePullSecret(&PullSecretOptions{ + Namespace: defaultNamespace, + RegistryURL: pullSecret.Registry, + Username: username, + PasswordOrToken: password, + Email: email, + Secret: pullSecret.Secret, + }) + if err != nil { + return err + } + + // create pull secrets in other namespaces if there are any + namespaces := map[string]bool{ + defaultNamespace: true, + } + for _, deployConfig := range r.config.Deployments { + if deployConfig.Namespace == "" || namespaces[deployConfig.Namespace] { + continue + } + + err := r.CreatePullSecret(&PullSecretOptions{ + Namespace: deployConfig.Namespace, + RegistryURL: pullSecret.Registry, + Username: username, + PasswordOrToken: password, + Email: email, + Secret: pullSecret.Secret, + }) + if err != nil { + return err + } + + namespaces[deployConfig.Namespace] = true + } + } + + return nil +} diff --git a/pkg/devspace/registry/init_test.go b/pkg/devspace/pullsecrets/init_test.go similarity index 98% rename from pkg/devspace/registry/init_test.go rename to pkg/devspace/pullsecrets/init_test.go index d766c4e03d..192868ef22 100644 --- a/pkg/devspace/registry/init_test.go +++ b/pkg/devspace/pullsecrets/init_test.go @@ -1,7 +1,6 @@ -package registry +package pullsecrets -import ( -) +import () /*type createPullSecretTestCase struct { name string @@ -98,6 +97,6 @@ StopWait`, "email": "someuser@example.com" } } - }`, string(resultSecret.Data[k8sv1.DockerConfigJsonKey]), "Saved secret has wrong data")*//* + }`, string(resultSecret.Data[k8sv1.DockerConfigJsonKey]), "Saved secret has wrong data")*/ /* }*/ diff --git a/pkg/devspace/registry/registry.go b/pkg/devspace/pullsecrets/registry.go similarity index 54% rename from pkg/devspace/registry/registry.go rename to pkg/devspace/pullsecrets/registry.go index d2cd0d2faa..ec4d09730b 100644 --- a/pkg/devspace/registry/registry.go +++ b/pkg/devspace/pullsecrets/registry.go @@ -1,10 +1,13 @@ -package registry +package pullsecrets import ( "context" "encoding/base64" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" "regexp" "strings" + "time" "github.com/pkg/errors" @@ -18,12 +21,21 @@ var registryNameReplaceRegex = regexp.MustCompile(`[^a-z0-9\\-]`) // PullSecretOptions has all options neccessary to create a pullSecret type PullSecretOptions struct { - Namespace, RegistryURL, Username, PasswordOrToken, Email string + Namespace string + RegistryURL string + Username string + PasswordOrToken string + Email string + Secret string } // CreatePullSecret creates an image pull secret for a registry func (r *client) CreatePullSecret(options *PullSecretOptions) error { - pullSecretName := GetRegistryAuthSecretName(options.RegistryURL) + pullSecretName := options.Secret + if pullSecretName == "" { + pullSecretName = GetRegistryAuthSecretName(options.RegistryURL) + } + if options.RegistryURL == "hub.docker.com" || options.RegistryURL == "" { options.RegistryURL = "https://index.docker.io/v1/" } @@ -55,19 +67,30 @@ func (r *client) CreatePullSecret(options *PullSecretOptions) error { Type: k8sv1.SecretTypeDockerConfigJson, } - secret, err := r.kubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Get(context.TODO(), pullSecretName, metav1.GetOptions{}) - if err != nil { - _, err = r.kubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Create(context.TODO(), registryPullSecret, metav1.CreateOptions{}) + err := wait.PollImmediate(time.Second, time.Second*30, func() (bool, error) { + secret, err := r.kubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Get(context.TODO(), pullSecretName, metav1.GetOptions{}) if err != nil { - return errors.Errorf("Unable to create image pull secret: %s", err.Error()) - } + _, err = r.kubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Create(context.TODO(), registryPullSecret, metav1.CreateOptions{}) + if err != nil { + return false, errors.Errorf("Unable to create image pull secret: %s", err.Error()) + } - r.log.Donef("Created image pull secret %s/%s", options.Namespace, pullSecretName) - } else if secret.Data == nil || string(secret.Data[pullSecretDataKey]) != string(pullSecretData[pullSecretDataKey]) { - _, err = r.kubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Update(context.TODO(), registryPullSecret, metav1.UpdateOptions{}) - if err != nil { - return errors.Errorf("Unable to update image pull secret: %s", err.Error()) + r.log.Donef("Created image pull secret %s/%s", options.Namespace, pullSecretName) + } else if secret.Data == nil || string(secret.Data[pullSecretDataKey]) != string(pullSecretData[pullSecretDataKey]) { + _, err = r.kubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Update(context.TODO(), registryPullSecret, metav1.UpdateOptions{}) + if err != nil { + if kerrors.IsConflict(err) { + return false, nil + } + + return false, errors.Errorf("Unable to update image pull secret: %s", err.Error()) + } } + + return true, nil + }) + if err != nil { + return errors.Wrap(err, "create pull secret") } return nil diff --git a/pkg/devspace/registry/registry_test.go b/pkg/devspace/pullsecrets/registry_test.go similarity index 98% rename from pkg/devspace/registry/registry_test.go rename to pkg/devspace/pullsecrets/registry_test.go index f9bd2694f5..9686b29724 100644 --- a/pkg/devspace/registry/registry_test.go +++ b/pkg/devspace/pullsecrets/registry_test.go @@ -1,4 +1,4 @@ -package registry +package pullsecrets import () diff --git a/pkg/devspace/registry/testing/fake.go b/pkg/devspace/pullsecrets/testing/fake.go similarity index 66% rename from pkg/devspace/registry/testing/fake.go rename to pkg/devspace/pullsecrets/testing/fake.go index f7b531a4fa..7f17a4bb4c 100644 --- a/pkg/devspace/registry/testing/fake.go +++ b/pkg/devspace/pullsecrets/testing/fake.go @@ -1,6 +1,6 @@ package testing -import "github.com/devspace-cloud/devspace/pkg/devspace/registry" +import "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" // Client is a fake implementation of the Client interface type Client struct{} @@ -11,6 +11,6 @@ func (c *Client) CreatePullSecrets() error { } // CreatePullSecret is a fake implementation of the function -func (c *Client) CreatePullSecret(options *registry.PullSecretOptions) error { +func (c *Client) CreatePullSecret(options *pullsecrets.PullSecretOptions) error { return nil } diff --git a/pkg/devspace/registry/util.go b/pkg/devspace/pullsecrets/util.go similarity index 98% rename from pkg/devspace/registry/util.go rename to pkg/devspace/pullsecrets/util.go index f1dc3ef0e2..378583682d 100644 --- a/pkg/devspace/registry/util.go +++ b/pkg/devspace/pullsecrets/util.go @@ -1,4 +1,4 @@ -package registry +package pullsecrets import ( "strings" diff --git a/pkg/devspace/registry/util_test.go b/pkg/devspace/pullsecrets/util_test.go similarity index 90% rename from pkg/devspace/registry/util_test.go rename to pkg/devspace/pullsecrets/util_test.go index f1197d48e0..80b20c3030 100644 --- a/pkg/devspace/registry/util_test.go +++ b/pkg/devspace/pullsecrets/util_test.go @@ -1,12 +1,12 @@ -package registry +package pullsecrets import ( "testing" - + "gotest.tools/assert" ) -func TestGetRegistryFromImageName(t *testing.T){ +func TestGetRegistryFromImageName(t *testing.T) { //Test with official repo reg, err := GetRegistryFromImageName("mysql") if err != nil { diff --git a/pkg/devspace/registry/init.go b/pkg/devspace/registry/init.go deleted file mode 100644 index c5df7c01bd..0000000000 --- a/pkg/devspace/registry/init.go +++ /dev/null @@ -1,132 +0,0 @@ -package registry - -import ( - "context" - "strings" - - "github.com/pkg/errors" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// CreatePullSecrets creates the image pull secrets -func (r *client) CreatePullSecrets() error { - if r.config.Images != nil { - pullSecrets := []string{} - createPullSecrets := map[string]bool{} - - for _, imageConf := range r.config.Images { - if imageConf.CreatePullSecret == nil || *imageConf.CreatePullSecret == true { - registryURL, err := GetRegistryFromImageName(imageConf.Image) - if err != nil { - return err - } - - createPullSecrets[registryURL] = true - } - } - - for registryURL := range createPullSecrets { - displayRegistryURL := registryURL - if displayRegistryURL == "" { - displayRegistryURL = "hub.docker.com" - } - - r.log.StartWait("Creating image pull secret for registry: " + displayRegistryURL) - err := r.createPullSecretForRegistry(registryURL) - r.log.StopWait() - if err != nil { - return errors.Errorf("Failed to create pull secret for registry: %v", err) - } - - pullSecrets = append(pullSecrets, GetRegistryAuthSecretName(registryURL)) - } - - if len(pullSecrets) > 0 { - err := r.addPullSecretsToServiceAccount(pullSecrets) - if err != nil { - return errors.Wrap(err, "add pull secrets to service account") - } - } - } - - return nil -} - -func (r *client) addPullSecretsToServiceAccount(pullSecrets []string) error { - // Get default service account - serviceaccount, err := r.kubeClient.KubeClient().CoreV1().ServiceAccounts(r.kubeClient.Namespace()).Get(context.TODO(), "default", metav1.GetOptions{}) - if err != nil { - r.log.Errorf("Couldn't find service account 'default' in namespace '%s': %v", r.kubeClient.Namespace(), err) - return nil - } - - // Check if all pull secrets are there - changed := false - for _, newPullSecret := range pullSecrets { - found := false - - for _, pullSecret := range serviceaccount.ImagePullSecrets { - if pullSecret.Name == newPullSecret { - found = true - break - } - } - - if found == false { - changed = true - serviceaccount.ImagePullSecrets = append(serviceaccount.ImagePullSecrets, v1.LocalObjectReference{Name: newPullSecret}) - } - } - - // Should we update the service account? - if changed { - _, err := r.kubeClient.KubeClient().CoreV1().ServiceAccounts(r.kubeClient.Namespace()).Update(context.TODO(), serviceaccount, metav1.UpdateOptions{}) - if err != nil { - if strings.Index(err.Error(), "the object has been modified; please apply your changes to the latest version and try again") != -1 { - r.log.Infof("Reapplying image pull secrets to service account %s", serviceaccount.Name) - return r.addPullSecretsToServiceAccount(pullSecrets) - } - - return errors.Wrap(err, "update service account") - } - } - - return nil -} - -func (r *client) createPullSecretForRegistry(registryURL string) error { - username, password := "", "" - if r.dockerClient != nil { - authConfig, _ := r.dockerClient.GetAuthConfig(registryURL, true) - if authConfig != nil { - username = authConfig.Username - password = authConfig.Password - } - } - - if r.config.Deployments != nil && username != "" && password != "" { - for _, deployConfig := range r.config.Deployments { - email := "noreply@devspace.cloud" - - namespace := r.kubeClient.Namespace() - if deployConfig.Namespace != "" { - namespace = deployConfig.Namespace - } - - err := r.CreatePullSecret(&PullSecretOptions{ - Namespace: namespace, - RegistryURL: registryURL, - Username: username, - PasswordOrToken: password, - Email: email, - }) - if err != nil { - return err - } - } - } - - return nil -} diff --git a/pkg/devspace/server/server.go b/pkg/devspace/server/server.go index a8910f9ce8..83f1ed25b8 100644 --- a/pkg/devspace/server/server.go +++ b/pkg/devspace/server/server.go @@ -8,7 +8,6 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl/portforward" "github.com/devspace-cloud/devspace/pkg/devspace/upgrade" - "github.com/devspace-cloud/devspace/pkg/util/analytics" "github.com/devspace-cloud/devspace/pkg/util/kubeconfig" "github.com/devspace-cloud/devspace/pkg/util/log" "github.com/devspace-cloud/devspace/pkg/util/port" @@ -163,11 +162,6 @@ func newHandler(configLoader loader.ConfigLoader, config *latest.Config, generat terminalResizeQueues: make(map[string]TerminalResizeQueue), } - analytics, err := analytics.GetAnalytics() - if err == nil { - handler.analyticsEnabled = analytics.Enabled() - } - // Load raw config if config != nil { handler.rawConfig, err = configLoader.LoadRaw() diff --git a/pkg/devspace/sync/sync.go b/pkg/devspace/sync/sync.go index 58af8f14c6..2dd0b5f933 100644 --- a/pkg/devspace/sync/sync.go +++ b/pkg/devspace/sync/sync.go @@ -1,7 +1,6 @@ package sync import ( - "fmt" "io" "os" "path/filepath" @@ -10,7 +9,6 @@ import ( "time" "github.com/devspace-cloud/devspace/pkg/devspace/config/versions/latest" - "github.com/devspace-cloud/devspace/pkg/util/analytics/cloudanalytics" "github.com/devspace-cloud/devspace/pkg/util/log" "github.com/pkg/errors" @@ -380,9 +378,6 @@ func (s *Sync) Stop(fatalError error) { // This needs to be rethought because we do not always kill the application here, would be better to have an error channel // or runtime error here - sendError := fmt.Errorf("Fatal sync error: %v. For more information check .devspace/logs/sync.log", fatalError) - cloudanalytics.SendCommandEvent(sendError) - if s.Options.SyncError != nil { s.Options.SyncError <- fatalError } diff --git a/pkg/devspace/upgrade/upgrade.go b/pkg/devspace/upgrade/upgrade.go index 7da069dbf8..9867ae134c 100644 --- a/pkg/devspace/upgrade/upgrade.go +++ b/pkg/devspace/upgrade/upgrade.go @@ -9,7 +9,6 @@ import ( "github.com/devspace-cloud/devspace/pkg/util/log" "github.com/blang/semver" - "github.com/devspace-cloud/devspace/pkg/util/analytics/cloudanalytics" "github.com/rhysd/go-github-selfupdate/selfupdate" ) @@ -65,9 +64,6 @@ func SetVersion(verText string) { version = _version rawVersion = verText } - - // Start analytics - cloudanalytics.Start(version) } var ( diff --git a/pkg/util/analytics/analytics.go b/pkg/util/analytics/analytics.go deleted file mode 100644 index 7fe944eac3..0000000000 --- a/pkg/util/analytics/analytics.go +++ /dev/null @@ -1,509 +0,0 @@ -package analytics - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "os/exec" - "os/signal" - "path/filepath" - "regexp" - "runtime" - "runtime/debug" - "strconv" - "strings" - "sync" - "time" - - "github.com/devspace-cloud/devspace/pkg/util/hash" - "github.com/devspace-cloud/devspace/pkg/util/kubeconfig" - "github.com/devspace-cloud/devspace/pkg/util/randutil" - "github.com/devspace-cloud/devspace/pkg/util/yamlutil" - "github.com/google/uuid" - homedir "github.com/mitchellh/go-homedir" - "github.com/pkg/errors" - "github.com/shirou/gopsutil/process" -) - -var token string -var eventEndpoint string -var userEndpoint string -var analyticsConfigFile string -var analyticsInstance *analyticsConfig -var loadAnalyticsOnce sync.Once - -const DEFERRED_REQUEST_COMMAND = "send-deferred-request" - -// Analytics is an interface for sending data to an analytics service -type Analytics interface { - Enabled() bool - Disable() error - Enable() error - SendEvent(eventName string, eventProperties map[string]interface{}, userProperties map[string]interface{}) error - SendCommandEvent(commandArgs []string, commandError error, commandDuration int64) error - ReportPanics() - Identify(identifier string) error - SetVersion(version string) - SetIdentifyProvider(getIdentity func() string) - HandleDeferredRequest() error -} - -type analyticsConfig struct { - DistinctID string `yaml:"distinctId,omitempty"` - Identifier string `yaml:"identifier,omitempty"` - Disabled bool `yaml:"disabled,omitempty"` - LatestUpdate int64 `yaml:"latestUpdate,omitempty"` - LatestSession int64 `yaml:"latestSession,omitempty"` - - version string - identityProvider *func() string - - kubeLoader kubeconfig.Loader -} - -func (a *analyticsConfig) Enabled() bool { - return !a.Disabled -} - -func (a *analyticsConfig) Disable() error { - if !a.Disabled { - identValue := map[string]interface{}{ - "device_id": a.DistinctID, - - "user_properties": map[string]interface{}{ - "enabled": false, - }, - } - - if a.Identifier != "" { - identValue["user_id"] = a.Identifier - } - - requestData := map[string]interface{}{ - "parameters": map[string]interface{}{ - "api_key": token, - "identification": []interface{}{ - identValue, - }, - }, - } - - err := a.sendRequest(userEndpoint, requestData) - if err != nil { - // ignore if request fails - } - - a.Disabled = true - return a.save() - } - return nil -} - -func (a *analyticsConfig) Enable() error { - if a.Disabled { - a.Disabled = false - return a.save() - } - return nil -} - -func (a *analyticsConfig) Identify(identifier string) error { - if identifier != a.Identifier { - if a.Identifier != "" { - // user was logged in, now different user is logging in => RESET DISTINCT ID - _ = a.resetDistinctID() - } - a.Identifier = identifier - - requestData := map[string]interface{}{ - "parameters": map[string]interface{}{ - "api_key": token, - "identification": []interface{}{ - map[string]interface{}{ - "device_id": a.DistinctID, - "user_id": a.Identifier, - }, - }, - }, - } - - return a.sendRequest(userEndpoint, requestData) - } - return nil -} - -func (a *analyticsConfig) SendCommandEvent(commandArgs []string, commandError error, commandDuration int64) error { - executable, _ := os.Executable() - command := strings.Join(commandArgs, " ") - command = strings.Replace(command, executable, "devspace", 1) - - expr := regexp.MustCompile(`^.*\s+(login\s.*--key=?\s*)(.*)(\s.*|$)`) - command = expr.ReplaceAllString(command, `devspace $1[REDACTED]$3`) - - userProperties := map[string]interface{}{ - "app_version": a.version, - } - commandProperties := map[string]interface{}{ - "command": command, - "runtime_os": runtime.GOOS, - "runtime_arch": runtime.GOARCH, - "cli_version": a.version, - } - - if commandError != nil { - commandProperties["error"] = strings.Replace(commandError.Error(), "\n", "\\n", -1) - } - - contextName, err := a.kubeLoader.GetCurrentContext() - if contextName != "" && err == nil { - spaceID, cloudProvider, _ := a.kubeLoader.GetSpaceID(contextName) - - if spaceID != 0 { - commandProperties["space_id"] = spaceID - commandProperties["cloud_provider"] = cloudProvider - userProperties["has_spaces"] = true - } - - kubeConfig, err := a.kubeLoader.LoadRawConfig() - if err == nil { - if context, ok := kubeConfig.Contexts[contextName]; ok { - if cluster, ok := kubeConfig.Clusters[context.Cluster]; ok { - commandProperties["kube_server"] = cluster.Server - } - - commandProperties["kube_namespace"] = hash.String(context.Namespace) - } - } - commandProperties["kube_context"] = hash.String(contextName) - } - - if commandDuration != 0 { - commandProperties["duration"] = strconv.FormatInt(commandDuration, 10) + "ms" - } - - if regexp.MustCompile(`^.*\s+(use\s+space\s.*--get-token((\s*)|$))`).MatchString(command) { - return a.SendEvent("kube-context", commandProperties, userProperties) - } - return a.SendEvent("command", commandProperties, userProperties) -} - -func (a *analyticsConfig) SendEvent(eventName string, eventProperties map[string]interface{}, userProperties map[string]interface{}) error { - if !a.Disabled && token != "" { - now := time.Now() - - insertID, err := randutil.GenerateRandomString(9) - if err != nil { - return errors.Errorf("Couldn't generate random insert_id for analytics: %v", err) - } - eventData := map[string]interface{}{} - eventData["insert_id"] = insertID + strconv.FormatInt(now.Unix(), 16) - eventData["event_type"] = eventName - eventData["ip"] = "$remote" - - if _, ok := eventData["app_version"]; !ok { - eventData["app_version"] = a.version - } - - if _, ok := eventData["session_id"]; !ok { - sessionID, err := a.getSessionID() - if err != nil { - return err - } - eventData["session_id"] = sessionID - } - - if a.identityProvider != nil { - getIdentity := *a.identityProvider - a.Identify(getIdentity()) - } - - userProperties["enabled"] = true - - if a.Identifier != "" { - eventData["user_id"] = a.Identifier - eventData["user_properties"] = userProperties - } else { - eventData["device_id"] = a.DistinctID - } - - // save session and identity - err = a.save() - if err != nil { - // ignore if save fails - } - - eventData["event_properties"] = eventProperties - - requestBody := map[string]interface{}{} - requestBody["api_key"] = token - requestBody["events"] = []interface{}{ - eventData, - } - requestData := map[string]interface{}{ - "body": requestBody, - } - - return a.sendRequest(eventEndpoint, requestData) - } - return nil -} - -func (a *analyticsConfig) getSessionID() (int64, error) { - now := time.Now() - sessionExpired := time.Unix(a.LatestUpdate*int64(time.Millisecond), 0).Add(time.Minute * 30).Before(now) - a.LatestUpdate = now.UnixNano() / int64(time.Millisecond) - - if a.LatestSession == 0 || sessionExpired { - a.LatestSession = a.LatestUpdate - (a.LatestUpdate % (24 * 60 * 60 * 1000)) - } - return a.LatestSession, nil -} - -func (a *analyticsConfig) ReportPanics() { - if r := recover(); r != nil { - err := errors.Errorf("Panic: %v\n%v", r, string(debug.Stack())) - - a.SendCommandEvent(os.Args, err, GetProcessDuration()) - } -} - -func (a *analyticsConfig) SetVersion(version string) { - if version == "" { - version = "dev" - } - a.version = version -} - -func (a *analyticsConfig) SetIdentifyProvider(getIdentity func() string) { - a.identityProvider = &getIdentity -} - -func (a *analyticsConfig) sendRequest(requestURL string, data map[string]interface{}) error { - if !a.Disabled && token != "" { - var err error - jsonRequestBody := []byte{} - requestURL, err := url.Parse(requestURL) - - if requestBody, ok := data["body"]; ok { - jsonRequestBody, err = json.Marshal(requestBody) - if err != nil { - return errors.Errorf("Couldn't marshal analytics data to json: %v", err) - } - } - - if requestParams, ok := data["parameters"]; ok { - params := url.Values{} - paramsMap := requestParams.(map[string]interface{}) - for key := range paramsMap { - paramValueMap, isMap := paramsMap[key].(map[string]interface{}) - paramValueArray, isArray := paramsMap[key].([]interface{}) - if isMap || isArray { - var paramValue interface{} - if isMap { - paramValue = paramValueMap - } - if isArray { - paramValue = paramValueArray - } - jsonParam, err := json.Marshal(paramValue) - if err != nil { - return errors.Errorf("Couldn't marshal analytics data to json: %v", err) - } - params.Add(key, string(jsonParam)) - } else { - params.Add(key, paramsMap[key].(string)) - } - } - requestURL.RawQuery = params.Encode() - } - - headers := map[string][]string{ - "Content-Type": []string{"application/json"}, - "Accept": []string{"*/*"}, - } - jsonHeaders, err := json.Marshal(headers) - if err != nil { - return errors.Errorf("Couldn't marshal analytics headers: %v", err) - } - - args := []string{DEFERRED_REQUEST_COMMAND, "POST", base64.StdEncoding.EncodeToString([]byte(requestURL.String())), base64.StdEncoding.EncodeToString(jsonHeaders), base64.StdEncoding.EncodeToString(jsonRequestBody)} - executable, err := os.Executable() - if err != nil { - executable = os.Args[0] - } - - cmd := exec.Command(executable, args...) - - return cmd.Start() - } - return nil -} - -// HandleDeferredRequest sends a request if args are: executable, DEFERRED_REQUEST_COMMAND -func (a *analyticsConfig) HandleDeferredRequest() error { - if len(os.Args) < 5 || os.Args[1] != DEFERRED_REQUEST_COMMAND { - return nil - } - - httpMethod := os.Args[2] - requestURL, err := base64.StdEncoding.DecodeString(os.Args[3]) - if err != nil { - return errors.Errorf("Couldn't base64.decode request URL: %v", err) - } - - jsonRequestHeaders, err := base64.StdEncoding.DecodeString(os.Args[4]) - if err != nil { - return errors.Errorf("Couldn't base64.decode request headers: %v", err) - } - - requestBody := []byte{} - if len(os.Args) > 5 { - requestBody, err = base64.StdEncoding.DecodeString(os.Args[5]) - if err != nil { - return errors.Errorf("Couldn't base64.decode request body: %v", err) - } - } - - requestHeaders := map[string][]string{} - err = json.Unmarshal([]byte(jsonRequestHeaders), &requestHeaders) - if err != nil { - return errors.Errorf("Couldn't unmarshal request headers: %v", err) - } - - request, err := http.NewRequest(httpMethod, string(requestURL), bytes.NewBuffer(requestBody)) - if err != nil { - return errors.Errorf("Error creating request to analytics endpoint: %v", err) - } - request.Header = requestHeaders - client := &http.Client{} - - response, err := client.Do(request) - if err != nil { - return errors.Errorf("Error sending request to analytics endpoint: %v", err) - } - defer response.Body.Close() - body, _ := ioutil.ReadAll(response.Body) - - if response.StatusCode != 200 { - return fmt.Errorf("Analytics returned HTTP code %d: %s", response.StatusCode, body) - } - - os.Exit(0) - - return nil -} - -func (a *analyticsConfig) resetDistinctID() error { - DistinctID, err := uuid.NewRandom() - if err != nil { - return errors.Errorf("Couldn't create UUID: %v", err) - } - a.DistinctID = DistinctID.String() - - return nil -} - -func (a *analyticsConfig) save() error { - analyticsConfigFilePath, err := a.getAnalyticsConfigFilePath() - if err != nil { - return errors.Errorf("Couldn't determine config file: %v", err) - } - - err = yamlutil.WriteYamlToFile(a, analyticsConfigFilePath) - if err != nil { - return errors.Errorf("Couldn't save analytics config file %s: %v", analyticsConfigFilePath, err) - } - return nil -} - -func (a *analyticsConfig) getAnalyticsConfigFilePath() (string, error) { - homedir, err := homedir.Dir() - if err != nil { - return "", err - } - - return filepath.Join(homedir, analyticsConfigFile), nil -} - -// GetAnalytics retrieves the analytics client -func GetAnalytics() (Analytics, error) { - var err error - - loadAnalyticsOnce.Do(func() { - analyticsInstance = &analyticsConfig{ - kubeLoader: kubeconfig.NewLoader(), - } - - analyticsConfigFilePath, err := analyticsInstance.getAnalyticsConfigFilePath() - if err != nil { - err = errors.Errorf("Couldn't determine config file: %v", err) - return - } - _, err = os.Stat(analyticsConfigFilePath) - if err == nil { - err := yamlutil.ReadYamlFromFile(analyticsConfigFilePath, analyticsInstance) - if err != nil { - err = errors.Errorf("Couldn't read analytics config file %s: %v", analyticsConfigFilePath, err) - return - } - } - - if analyticsInstance.DistinctID == "" { - err = analyticsInstance.resetDistinctID() - if err != nil { - err = errors.Errorf("Couldn't reset analytics distinct id: %v", err) - return - } - - err = analyticsInstance.save() - if err != nil { - err = errors.Errorf("Couldn't save analytics config: %v", err) - return - } - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - - go func() { - defer func() { - if r := recover(); r != nil { - // Fail silently - } - }() - - <-c - - analyticsInstance.SendCommandEvent(os.Args, errors.New("interrupted"), GetProcessDuration()) - signal.Stop(c) - - pid := os.Getpid() - sigterm(pid) - }() - }) - return analyticsInstance, err -} - -// SetConfigPath sets the config patch -func SetConfigPath(path string) { - analyticsConfigFile = path -} - -// GetProcessDuration returns the process duration -func GetProcessDuration() int64 { - pid := os.Getpid() - p, err := process.NewProcess(int32(pid)) - if err == nil { - procCreateTime, err := p.CreateTime() - if err == nil { - return time.Now().UnixNano()/int64(time.Millisecond) - procCreateTime - } - } - - return 0 -} diff --git a/pkg/util/analytics/cloudanalytics/start.go b/pkg/util/analytics/cloudanalytics/start.go deleted file mode 100644 index 5bdbd27fda..0000000000 --- a/pkg/util/analytics/cloudanalytics/start.go +++ /dev/null @@ -1,67 +0,0 @@ -package cloudanalytics - -import ( - "github.com/devspace-cloud/devspace/pkg/devspace/config/constants" - "github.com/devspace-cloud/devspace/pkg/util/analytics" - "os" -) - -// ReportPanics resolves a panic -func ReportPanics() { - defer func() { - if r := recover(); r != nil { - // Fail silently - } - }() - - a, err := analytics.GetAnalytics() - if err != nil { - return - } - - a.ReportPanics() -} - -// SendCommandEvent sends a new event to the analytics provider -func SendCommandEvent(commandErr error) { - defer func() { - if r := recover(); r != nil { - // Fail silently - } - }() - - a, err := analytics.GetAnalytics() - if err != nil { - return - } - - a.SendCommandEvent(os.Args, commandErr, analytics.GetProcessDuration()) -} - -// Start initializes the analytics -func Start(version string) { - defer func() { - if r := recover(); r != nil { - // Fail silently - } - }() - - analytics.SetConfigPath(constants.DefaultHomeDevSpaceFolder + "/analytics.yaml") - - analytics, err := analytics.GetAnalytics() - if err != nil { - return - } - - analytics.SetVersion(version) - analytics.SetIdentifyProvider(getIdentity) - err = analytics.HandleDeferredRequest() - if err != nil { - // ignore error - } -} - -// getIdentity return the cloud identifier -func getIdentity() string { - return "" -} diff --git a/pkg/util/analytics/sigterm.go b/pkg/util/analytics/sigterm.go deleted file mode 100644 index 374379cd23..0000000000 --- a/pkg/util/analytics/sigterm.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build !windows - -package analytics - -import ( - "os" -) - -func sigterm(pid int) { - p, err := os.FindProcess(pid) - if err != nil { - return - } - - p.Signal(os.Interrupt) -} diff --git a/pkg/util/analytics/sigterm_win.go b/pkg/util/analytics/sigterm_win.go deleted file mode 100644 index 44c60b4ee6..0000000000 --- a/pkg/util/analytics/sigterm_win.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build windows - -package analytics - -import ( - "os" - "syscall" -) - -func sigterm(pid int) { - d, e := syscall.LoadDLL("kernel32.dll") - if e != nil { - return - } - - p, e := d.FindProc("GenerateConsoleCtrlEvent") - if e != nil { - return - } - - r, _, _ := p.Call(uintptr(syscall.CTRL_C_EVENT), uintptr(pid)) - if r != 0 { - os.Exit(1) - } -} diff --git a/pkg/util/factory/factory.go b/pkg/util/factory/factory.go index 15e479dfee..62541ba927 100644 --- a/pkg/util/factory/factory.go +++ b/pkg/util/factory/factory.go @@ -15,7 +15,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/hook" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" "github.com/devspace-cloud/devspace/pkg/devspace/plugin" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/devspace/services" "github.com/devspace-cloud/devspace/pkg/devspace/services/targetselector" "github.com/devspace-cloud/devspace/pkg/util/kubeconfig" @@ -45,7 +45,7 @@ type Factory interface { NewHookExecutor(config *latest.Config) hook.Executer // Pull secrets client - NewPullSecretClient(config *latest.Config, kubeClient kubectl.Client, dockerClient docker.Client, log log.Logger) registry.Client + NewPullSecretClient(config *latest.Config, kubeClient kubectl.Client, dockerClient docker.Client, log log.Logger) pullsecrets.Client // Docker NewDockerClient(log log.Logger) (docker.Client, error) @@ -120,8 +120,8 @@ func (f *DefaultFactoryImpl) NewDependencyManager(config *latest.Config, cache * } // NewPullSecretClient implements interface -func (f *DefaultFactoryImpl) NewPullSecretClient(config *latest.Config, kubeClient kubectl.Client, dockerClient docker.Client, log log.Logger) registry.Client { - return registry.NewClient(config, kubeClient, dockerClient, log) +func (f *DefaultFactoryImpl) NewPullSecretClient(config *latest.Config, kubeClient kubectl.Client, dockerClient docker.Client, log log.Logger) pullsecrets.Client { + return pullsecrets.NewClient(config, kubeClient, dockerClient, log) } // NewConfigLoader implements interface diff --git a/pkg/util/factory/testing/factory.go b/pkg/util/factory/testing/factory.go index 5f6118464e..2176d939c0 100644 --- a/pkg/util/factory/testing/factory.go +++ b/pkg/util/factory/testing/factory.go @@ -14,7 +14,7 @@ import ( "github.com/devspace-cloud/devspace/pkg/devspace/hook" "github.com/devspace-cloud/devspace/pkg/devspace/kubectl" "github.com/devspace-cloud/devspace/pkg/devspace/plugin" - "github.com/devspace-cloud/devspace/pkg/devspace/registry" + "github.com/devspace-cloud/devspace/pkg/devspace/pullsecrets" "github.com/devspace-cloud/devspace/pkg/devspace/services" "github.com/devspace-cloud/devspace/pkg/devspace/services/targetselector" "github.com/devspace-cloud/devspace/pkg/util/factory" @@ -34,7 +34,7 @@ type Factory struct { Log log.Logger HookExecutor hook.Executer DependencyManager dependency.Manager - PullSecretClient registry.Client + PullSecretClient pullsecrets.Client ConfigLoader loader.ConfigLoader ConfigureManager configure.Manager DockerClient docker.Client @@ -85,7 +85,7 @@ func (f *Factory) NewDependencyManager(config *latest.Config, cache *generated.C } // NewPullSecretClient implements interface -func (f *Factory) NewPullSecretClient(config *latest.Config, kubeClient kubectl.Client, dockerClient docker.Client, log log.Logger) registry.Client { +func (f *Factory) NewPullSecretClient(config *latest.Config, kubeClient kubectl.Client, dockerClient docker.Client, log log.Logger) pullsecrets.Client { return f.PullSecretClient } diff --git a/ui/package.json b/ui/package.json index 77961a0644..fdd36f19c2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -67,9 +67,6 @@ "whatwg-fetch": "2.0.3", "xterm": "4.1.0" }, - "optionalDependencies": { - "@devspace/react-components": "0.1.5" - }, "scripts": { "start": "cross-env NODE_ENV=development node scripts/start.js", "build": "node scripts/build.js" diff --git a/ui/src/components/advanced/SupportChat/SupportChat.tsx b/ui/src/components/advanced/SupportChat/SupportChat.tsx deleted file mode 100644 index 0ba5bbf9ab..0000000000 --- a/ui/src/components/advanced/SupportChat/SupportChat.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import withDevSpaceConfig, { DevSpaceConfigContext } from 'contexts/withDevSpaceConfig/withDevSpaceConfig'; - -interface Props extends DevSpaceConfigContext {} -interface State {} - -class SupportChat extends React.PureComponent { - - render() { - let optionalComponent; - - try { - const Chat = require("@devspace/react-components").Chat; - const Analytics = require("@devspace/react-components").Analytics; - - optionalComponent = ( -
- - {this.props.devSpaceConfig.analyticsEnabled && - - } -
- ); - } catch (e) { - console.log("Not loading optional components in dev mode.") - } - - return ( -
- - {optionalComponent} - -