package set import ( "fmt" "io" "os" "github.com/spf13/cobra" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" "github.com/openshift/origin/pkg/cmd/templates" cmdutil "github.com/openshift/origin/pkg/cmd/util" "github.com/openshift/origin/pkg/cmd/util/clientcmd" deployapi "github.com/openshift/origin/pkg/deploy/api" ) var ( deploymentHookLong = templates.LongDesc(` Set or remove a deployment hook on a deployment config Deployment configs allow hooks to execute at different points in the lifecycle of the deployment, depending on the deployment strategy. For deployments with a Recreate strategy, a Pre, Mid, and Post hook can be specified. The Pre hook will execute before the deployment starts. The Mid hook will execute once the previous deployment has been scaled down to 0, but before the new one ramps up. The Post hook will execute once the deployment has completed. For deployments with a Rolling strategy, a Pre and Post hook can be specified. The Pre hook will execute before the deployment starts and the Post hook will execute once the deployment has completed. For each hook, a new pod will be started using one of the containers in the deployment's pod template with a specific command to execute. Additional environment variables may be specified for the hook, as well as which volumes from the pod template will be mounted on the hook pod. Each hook can have its own cancellation policy. One of: abort, retry, or ignore. Not all cancellation policies can be set on all hooks. For example, a Post hook on a rolling strategy does not support the abort policy, because at that point the deployment has already happened.`) deploymentHookExample = templates.Examples(` # Clear pre and post hooks on a deployment config %[1]s deployment-hook dc/myapp --remove --pre --post # Set the pre deployment hook to execute a db migration command for an application # using the data volume from the application %[1]s deployment-hook dc/myapp --pre -v data -- /var/lib/migrate-db.sh # Set a mid deployment hook along with additional environment variables %[1]s deployment-hook dc/myapp --mid -v data -e VAR1=value1 -e VAR2=value2 -- /var/lib/prepare-deploy.sh`) ) type DeploymentHookOptions struct { Out io.Writer Err io.Writer Builder *resource.Builder Infos []*resource.Info Encoder runtime.Encoder OutputVersion unversioned.GroupVersion Filenames []string Container string Selector string All bool ShortOutput bool Local bool Mapper meta.RESTMapper PrintObject func([]*resource.Info) error Pre bool Mid bool Post bool Remove bool Command []string Environment []string Volumes []string FailurePolicy deployapi.LifecycleHookFailurePolicy } // NewCmdDeploymentHook implements the set deployment-hook command func NewCmdDeploymentHook(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command { options := &DeploymentHookOptions{ Out: out, Err: errOut, } cmd := &cobra.Command{ Use: "deployment-hook DEPLOYMENTCONFIG --pre|--post|--mid -- CMD", Short: "Update a deployment hook on a deployment config", Long: deploymentHookLong, Example: fmt.Sprintf(deploymentHookExample, fullName), Run: func(cmd *cobra.Command, args []string) { kcmdutil.CheckErr(options.Complete(f, cmd, args)) kcmdutil.CheckErr(options.Validate()) if err := options.Run(); err != nil { // TODO: move me to kcmdutil if err == cmdutil.ErrExit { os.Exit(1) } kcmdutil.CheckErr(err) } }, } kcmdutil.AddPrinterFlags(cmd) cmd.Flags().StringVarP(&options.Container, "container", "c", options.Container, "The name of the container in the selected deployment config to use for the deployment hook") cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter deployment configs") cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all deployment configs in the namespace") cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename, directory, or URL to file to use to edit the resource.") cmd.Flags().BoolVar(&options.Remove, "remove", options.Remove, "If true, remove the specified deployment hook(s).") cmd.Flags().BoolVar(&options.Pre, "pre", options.Pre, "Set or remove a pre deployment hook") cmd.Flags().BoolVar(&options.Mid, "mid", options.Mid, "Set or remove a mid deployment hook") cmd.Flags().BoolVar(&options.Post, "post", options.Post, "Set or remove a post deployment hook") cmd.Flags().StringSliceVarP(&options.Environment, "environment", "e", options.Environment, "Environment variables to use in the deployment hook pod") cmd.Flags().StringSliceVarP(&options.Volumes, "volumes", "v", options.Volumes, "Volumes from the pod template to use in the deployment hook pod") cmd.Flags().String("failure-policy", "ignore", "The failure policy for the deployment hook. Valid values are: abort,retry,ignore") cmd.Flags().BoolVar(&options.Local, "local", false, "If true, set deployment hook will NOT contact api-server but run locally.") cmd.MarkFlagFilename("filename", "yaml", "yml", "json") return cmd } func (o *DeploymentHookOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error { resources := args if i := cmd.ArgsLenAtDash(); i != -1 { resources = args[:i] o.Command = args[i:] } if len(o.Filenames) == 0 && len(args) < 1 { return kcmdutil.UsageError(cmd, "one or more deployment configs must be specified as <name> or dc/<name>") } cmdNamespace, explicit, err := f.DefaultNamespace() if err != nil { return err } clientConfig, err := f.ClientConfig() if err != nil { return err } o.OutputVersion, err = kcmdutil.OutputVersion(cmd, clientConfig.GroupVersion) if err != nil { return err } mapper, typer := f.Object(false) o.Builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(explicit, false, o.Filenames...). Flatten() if !o.Local { o.Builder = o.Builder. ResourceNames("deploymentconfigs", resources...). SelectorParam(o.Selector). Latest() if o.All { o.Builder.ResourceTypes("deploymentconfigs").SelectAllParam(o.All) } } output := kcmdutil.GetFlagString(cmd, "output") if len(output) != 0 || o.Local { o.PrintObject = func(infos []*resource.Info) error { return f.PrintResourceInfos(cmd, infos, o.Out) } } o.Encoder = f.JSONEncoder() o.ShortOutput = kcmdutil.GetFlagString(cmd, "output") == "name" o.Mapper = mapper failurePolicyString := kcmdutil.GetFlagString(cmd, "failure-policy") if len(failurePolicyString) > 0 { switch failurePolicyString { case "abort": o.FailurePolicy = deployapi.LifecycleHookFailurePolicyAbort case "ignore": o.FailurePolicy = deployapi.LifecycleHookFailurePolicyIgnore case "retry": o.FailurePolicy = deployapi.LifecycleHookFailurePolicyRetry default: return kcmdutil.UsageError(cmd, "valid values for --failure-policy are: abort, retry, ignore") } } return nil } func (o *DeploymentHookOptions) Validate() error { if o.Remove { if len(o.Command) > 0 || len(o.Volumes) > 0 || len(o.Environment) > 0 || len(o.Container) > 0 { return fmt.Errorf("--remove may not be used with any option except --pre, --mid, or --post") } if !o.Pre && !o.Mid && !o.Post { return fmt.Errorf("you must specify at least one of --pre, --mid, or --post with the --remove flag") } return nil } cnt := 0 if o.Pre { cnt++ } if o.Mid { cnt++ } if o.Post { cnt++ } if cnt == 0 || cnt > 1 { return fmt.Errorf("you must specify one of --pre, --mid, or --post") } if len(o.Command) == 0 { return fmt.Errorf("you must specify a command for the deployment hook") } return nil } func (o *DeploymentHookOptions) Run() error { infos := o.Infos singular := len(o.Infos) <= 1 if o.Builder != nil { loaded, err := o.Builder.Do().IntoSingular(&singular).Infos() if err != nil { return err } infos = loaded } patches := CalculatePatches(infos, o.Encoder, func(info *resource.Info) (bool, error) { dc, ok := info.Object.(*deployapi.DeploymentConfig) if !ok { return false, nil } updated, err := o.updateDeploymentConfig(dc) return updated, err }) if singular && len(patches) == 0 { return fmt.Errorf("%s/%s is not a deployment config or does not have an applicable strategy", infos[0].Mapping.Resource, infos[0].Name) } if o.PrintObject != nil { return o.PrintObject(infos) } failed := false for _, patch := range patches { info := patch.Info if patch.Err != nil { fmt.Fprintf(o.Err, "error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err) continue } if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { fmt.Fprintf(o.Err, "info: %s %q was not changed\n", info.Mapping.Resource, info.Name) continue } obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, kapi.StrategicMergePatchType, patch.Patch) if err != nil { fmt.Fprintf(o.Err, "error: %v\n", err) failed = true continue } info.Refresh(obj, true) kcmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, false, "updated") } if failed { return cmdutil.ErrExit } return nil } func (o *DeploymentHookOptions) updateDeploymentConfig(dc *deployapi.DeploymentConfig) (bool, error) { var ( err error updatedRecreate bool updatedRolling bool ) if dc.Spec.Strategy.RecreateParams != nil { updatedRecreate, err = o.updateRecreateParams(dc, dc.Spec.Strategy.RecreateParams) if err != nil { return false, err } } if dc.Spec.Strategy.RollingParams != nil { updatedRolling, err = o.updateRollingParams(dc, dc.Spec.Strategy.RollingParams) if err != nil { return false, err } } return updatedRecreate || updatedRolling, nil } func (o *DeploymentHookOptions) updateRecreateParams(dc *deployapi.DeploymentConfig, strategyParams *deployapi.RecreateDeploymentStrategyParams) (bool, error) { var updated bool if o.Remove { if o.Pre && strategyParams.Pre != nil { updated = true strategyParams.Pre = nil } if o.Mid && strategyParams.Mid != nil { updated = true strategyParams.Mid = nil } if o.Post && strategyParams.Post != nil { updated = true strategyParams.Post = nil } return updated, nil } hook, err := o.lifecycleHook(dc) if err != nil { return true, err } switch { case o.Pre: strategyParams.Pre = hook case o.Mid: strategyParams.Mid = hook case o.Post: strategyParams.Post = hook } return true, nil } func (o *DeploymentHookOptions) updateRollingParams(dc *deployapi.DeploymentConfig, strategyParams *deployapi.RollingDeploymentStrategyParams) (bool, error) { var updated bool if o.Remove { if o.Pre && strategyParams.Pre != nil { updated = true strategyParams.Pre = nil } if o.Post && strategyParams.Post != nil { updated = true strategyParams.Post = nil } return updated, nil } hook, err := o.lifecycleHook(dc) if err != nil { return true, err } switch { case o.Pre: strategyParams.Pre = hook case o.Post: strategyParams.Post = hook } return true, nil } func (o *DeploymentHookOptions) lifecycleHook(dc *deployapi.DeploymentConfig) (*deployapi.LifecycleHook, error) { hook := &deployapi.LifecycleHook{ FailurePolicy: o.FailurePolicy, ExecNewPod: &deployapi.ExecNewPodHook{ Command: o.Command, }, } if len(o.Container) > 0 { found := false for _, c := range dc.Spec.Template.Spec.Containers { if c.Name == o.Container { found = true break } } if !found { fmt.Fprintf(o.Err, "warning: deployment config %q does not have a container named %q\n", dc.Name, o.Container) } hook.ExecNewPod.ContainerName = o.Container } if len(hook.ExecNewPod.ContainerName) == 0 { hook.ExecNewPod.ContainerName = dc.Spec.Template.Spec.Containers[0].Name } if len(o.Environment) > 0 { env, _, err := cmdutil.ParseEnv(o.Environment, nil) if err != nil { return nil, err } hook.ExecNewPod.Env = env } if len(o.Volumes) > 0 { for _, v := range o.Volumes { found := false for _, podVolume := range dc.Spec.Template.Spec.Volumes { if podVolume.Name == v { found = true break } } if !found { fmt.Fprintf(o.Err, "warning: deployment config %q does not have a volume named %q\n", dc.Name, v) } } hook.ExecNewPod.Volumes = o.Volumes } return hook, nil }