From 0509daaa3390861feacfa590265a29a2aac13636 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 28 Oct 2015 22:39:21 -0400 Subject: [PATCH] Provide informational output in new-app and new-build --- hack/util.sh | 2 +- pkg/cmd/cli/cmd/newapp.go | 90 ++++++++--- pkg/cmd/cli/cmd/newbuild.go | 40 ++++- pkg/cmd/cli/describe/helpers.go | 5 + pkg/config/cmd/cmd.go | 9 +- pkg/generate/app/cmd/describe.go | 209 ++++++++++++++++++++++++++ pkg/generate/app/cmd/newapp.go | 154 +++++++------------ pkg/generate/app/componentref.go | 9 +- pkg/generate/app/dockerimagelookup.go | 5 +- pkg/generate/app/imageref.go | 6 + pkg/generate/app/pipeline.go | 2 - pkg/image/api/helper.go | 12 ++ test/cmd/newapp.sh | 2 +- 13 files changed, 401 insertions(+), 144 deletions(-) create mode 100644 pkg/generate/app/cmd/describe.go diff --git a/hack/util.sh b/hack/util.sh index 433103ac3c60..c968883826a0 100644 --- a/hack/util.sh +++ b/hack/util.sh @@ -220,7 +220,7 @@ function tryuntil { timeout=$(($(date +%s) + 90)) echo "++ Retrying until success or timeout: ${@}" while [ 1 ]; do - if eval "${@}" 2>&1 >/dev/null; then + if eval "${@}" >/dev/null 2>&1; then return 0 fi if [[ $(date +%s) -gt $timeout ]]; then diff --git a/pkg/cmd/cli/cmd/newapp.go b/pkg/cmd/cli/cmd/newapp.go index dc2e46c8ac23..e8f3d1d7d693 100644 --- a/pkg/cmd/cli/cmd/newapp.go +++ b/pkg/cmd/cli/cmd/newapp.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "io" + "io/ioutil" "os" "sort" "strings" @@ -17,6 +18,7 @@ import ( kcmd "k8s.io/kubernetes/pkg/kubectl/cmd" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/errors" "k8s.io/kubernetes/pkg/util/sets" @@ -115,6 +117,7 @@ To search templates, image streams, and Docker images that match the arguments p // NewCmdNewApplication implements the OpenShift cli new-app command func NewCmdNewApplication(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { config := newcmd.NewAppConfig() + config.Deploy = true cmd := &cobra.Command{ Use: "new-app (IMAGE | IMAGESTREAM | TEMPLATE | PATH | URL ...)", @@ -155,6 +158,7 @@ func NewCmdNewApplication(fullName string, f *clientcmd.Factory, out io.Writer) cmd.Flags().BoolVar(&config.AllowMissingImages, "allow-missing-images", false, "If true, indicates that referenced Docker images that cannot be found locally or in a registry should still be used.") cmd.Flags().BoolVar(&config.AllowSecretUse, "grant-install-rights", false, "If true, a component that requires access to your account may use your token to install software into your project. Only grant images you trust the right to run with your token.") cmd.Flags().BoolVar(&config.SkipGeneration, "no-install", false, "Do not attempt to run images that describe themselves as being installable") + cmd.Flags().BoolVar(&config.DryRun, "dry-run", false, "If true, do not actually create resources.") // TODO AddPrinterFlags disabled so that it doesn't conflict with our own "template" flag. // Need a better solution. @@ -170,10 +174,15 @@ func NewCmdNewApplication(fullName string, f *clientcmd.Factory, out io.Writer) // RunNewApplication contains all the necessary functionality for the OpenShift cli new-app command func RunNewApplication(fullName string, f *clientcmd.Factory, out io.Writer, c *cobra.Command, args []string, config *newcmd.AppConfig) error { output := cmdutil.GetFlagString(c, "output") + shortOutput := output == "name" if err := setupAppConfig(f, out, c, args, config); err != nil { return err } + if shortOutput || len(output) != 0 { + config.Out = ioutil.Discard + } + if config.Querying() { result, err := config.RunQuery() if err != nil { @@ -189,11 +198,15 @@ func RunNewApplication(fullName string, f *clientcmd.Factory, out io.Writer, c * if err := setAppConfigLabels(c, config); err != nil { return err } - result, err := config.RunAll() + result, err := config.Run() if err := handleRunError(c, err, fullName); err != nil { return err } + if len(config.Labels) == 0 && len(result.Name) > 0 { + config.Labels = map[string]string{"app": result.Name} + } + if err := setLabels(config.Labels, result); err != nil { return err } @@ -202,15 +215,41 @@ func RunNewApplication(fullName string, f *clientcmd.Factory, out io.Writer, c * return err } - if len(output) != 0 && output != "name" { + indent := " " + switch { + case shortOutput: + indent = "" + case len(output) != 0: return f.Factory.PrintObject(c, result.List, out) + case !result.GeneratedJobs: + if len(config.Labels) > 0 { + fmt.Fprintf(out, "--> Creating resources with label %s ...\n", labels.SelectorFromSet(config.Labels).String()) + } else { + fmt.Fprintf(out, "--> Creating resources ...\n") + } + } + if config.DryRun { + return nil } + mapper, _ := f.Object() + var afterFn func(*resource.Info, error) + switch { // only print success if we don't have installables - if err := createObjects(f, out, c.Out(), output == "name", !result.GeneratedJobs, result); err != nil { + case !result.GeneratedJobs: + afterFn = configcmd.NewPrintNameOrErrorAfterIndent(mapper, shortOutput, "created", out, c.Out(), indent) + default: + afterFn = configcmd.NewPrintErrorAfter(mapper, c.Out()) + } + + if err := createObjects(f, afterFn, result); err != nil { return err } + if !shortOutput && !result.GeneratedJobs { + fmt.Fprintf(out, "--> Success\n") + } + hasMissingRepo := false installing := []*kapi.Pod{} for _, item := range result.List.Items { @@ -221,7 +260,7 @@ func RunNewApplication(fullName string, f *clientcmd.Factory, out io.Writer, c * } case *buildapi.BuildConfig: if len(t.Spec.Triggers) > 0 { - fmt.Fprintf(c.Out(), "Build scheduled for %q - use the build-logs command to track its progress.\n", t.Name) + fmt.Fprintf(out, "%sBuild scheduled for %q - use the build-logs command to track its progress.\n", indent, t.Name) } case *imageapi.ImageStream: if len(t.Status.DockerImageRepository) == 0 { @@ -229,10 +268,15 @@ func RunNewApplication(fullName string, f *clientcmd.Factory, out io.Writer, c * continue } hasMissingRepo = true - fmt.Fprint(c.Out(), "WARNING: No Docker registry has been configured with the server. Automatic builds and deployments may not function.\n") + fmt.Fprintf(out, "%sWARNING: No Docker registry has been configured with the server. Automatic builds and deployments may not function.\n", indent) } } } + + if shortOutput { + return nil + } + switch { case len(installing) == 1: // TODO: should get this set on the config or up above @@ -244,16 +288,16 @@ func RunNewApplication(fullName string, f *clientcmd.Factory, out io.Writer, c * return followInstallation(f, jobInput, installing[0], kclient, out) case len(installing) > 1: for i := range installing { - fmt.Fprintf(c.Out(), "Track installation of %s with '%s logs %s'.\n", installing[i].Name, fullName, installing[i].Name) + fmt.Fprintf(out, "%sTrack installation of %s with '%s logs %s'.\n", indent, installing[i].Name, fullName, installing[i].Name) } case len(result.List.Items) > 0: - fmt.Fprintf(c.Out(), "Run '%s %s' to view your app.\n", fullName, StatusRecommendedName) + fmt.Fprintf(out, "%sRun '%s %s' to view your app.\n", indent, fullName, StatusRecommendedName) } return nil } func followInstallation(f *clientcmd.Factory, input string, pod *kapi.Pod, kclient kclient.Interface, out io.Writer) error { - fmt.Fprintf(out, "Installing %q with pod %q ...\n", input, pod.Name) + fmt.Fprintf(out, "--> Installing ...\n") // we cannot retrieve logs until the pod is out of pending // TODO: move this to the server side @@ -307,7 +351,9 @@ func installationStarted(c kclient.PodInterface, name string, s kclient.SecretsI if secret, err := s.Get(name); err == nil { if secret.Annotations[newcmd.GeneratedForJob] == "true" && secret.Annotations[newcmd.GeneratedForJobFor] == pod.Annotations[newcmd.GeneratedForJobFor] { - s.Delete(name) + if err := s.Delete(name); err != nil { + glog.V(4).Infof("Failed to delete install secret %s: %v", name, err) + } } } return true, nil @@ -325,8 +371,10 @@ func installationComplete(c kclient.PodInterface, name string, out io.Writer) wa } switch pod.Status.Phase { case kapi.PodSucceeded: - fmt.Fprintf(out, "Installation complete\n") - c.Delete(name, nil) + fmt.Fprintf(out, "--> Success\n") + if err := c.Delete(name, nil); err != nil { + glog.V(4).Infof("Failed to delete install pod %s: %v", name, err) + } return true, nil case kapi.PodFailed: return true, fmt.Errorf("installation of %q did not complete successfully", name) @@ -404,11 +452,6 @@ func setAnnotations(annotations map[string]string, result *newcmd.AppResult) err } func setLabels(labels map[string]string, result *newcmd.AppResult) error { - if len(labels) == 0 { - if len(result.Name) > 0 { - labels = map[string]string{"app": result.Name} - } - } for _, object := range result.List.Items { err := util.AddObjectLabels(object, labels) if err != nil { @@ -459,22 +502,18 @@ func retryBuildConfig(info *resource.Info, err error) runtime.Object { return nil } -func createObjects(f *clientcmd.Factory, out, errout io.Writer, shortOutput, includeSuccess bool, result *newcmd.AppResult) error { +func createObjects(f *clientcmd.Factory, after func(*resource.Info, error), result *newcmd.AppResult) error { mapper, typer := f.Factory.Object() bulk := configcmd.Bulk{ Mapper: mapper, Typer: typer, RESTClientFactory: f.Factory.RESTClient, + + After: after, // Retry is used to support previous versions of the API server that will // consider the presence of an unknown trigger type to be an error. Retry: retryBuildConfig, } - switch { - case includeSuccess: - bulk.After = configcmd.NewPrintNameOrErrorAfter(mapper, shortOutput, "created", out, errout) - default: - bulk.After = configcmd.NewPrintErrorAfter(mapper, errout) - } if errs := bulk.Create(result.List, result.Namespace); len(errs) != 0 { return errExit } @@ -492,7 +531,10 @@ func handleRunError(c *cobra.Command, err error, fullName string) error { } switch t := err.(type) { case newcmd.ErrRequiresExplicitAccess: - return fmt.Errorf("installing %q requires that you grant the image access to run with your credentials; if you trust the provided image, include the flag --grant-install-rights", t.Match.Value) + return fmt.Errorf(`installing %q requires that you grant the image access to run with your credentials + +You can see more information about the image by adding the --dry-run flag. +If you trust the provided image, include the flag --grant-install-rights.`, t.Match.Value) case newapp.ErrNoMatch: return fmt.Errorf(`%[1]v diff --git a/pkg/cmd/cli/cmd/newbuild.go b/pkg/cmd/cli/cmd/newbuild.go index acf4ee0c2d8c..d2f1b70be4c1 100644 --- a/pkg/cmd/cli/cmd/newbuild.go +++ b/pkg/cmd/cli/cmd/newbuild.go @@ -12,8 +12,10 @@ import ( buildapi "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/cmd/util/clientcmd" + configcmd "github.com/openshift/origin/pkg/config/cmd" newapp "github.com/openshift/origin/pkg/generate/app" newcmd "github.com/openshift/origin/pkg/generate/app/cmd" + "k8s.io/kubernetes/pkg/labels" ) const ( @@ -104,6 +106,7 @@ func NewCmdNewBuild(fullName string, f *clientcmd.Factory, in io.Reader, out io. cmd.Flags().StringP("labels", "l", "", "Label to set in all generated resources.") cmd.Flags().BoolVar(&config.AllowMissingImages, "allow-missing-images", false, "If true, indicates that referenced Docker images that cannot be found locally or in a registry should still be used.") cmd.Flags().StringVar(&config.ContextDir, "context-dir", "", "Context directory to be used for the build.") + cmd.Flags().BoolVar(&config.DryRun, "dry-run", false, "If true, do not actually create resources.") cmdutil.AddPrinterFlags(cmd) return cmd @@ -112,6 +115,7 @@ func NewCmdNewBuild(fullName string, f *clientcmd.Factory, in io.Reader, out io. // RunNewBuild contains all the necessary functionality for the OpenShift cli new-build command func RunNewBuild(fullName string, f *clientcmd.Factory, out io.Writer, in io.Reader, c *cobra.Command, args []string, config *newcmd.AppConfig) error { output := cmdutil.GetFlagString(c, "output") + shortOutput := output == "name" if config.Dockerfile == "-" { data, err := ioutil.ReadAll(in) @@ -128,31 +132,57 @@ func RunNewBuild(fullName string, f *clientcmd.Factory, out io.Writer, in io.Rea if err := setAppConfigLabels(c, config); err != nil { return err } - result, err := config.RunBuilds() + result, err := config.Run() if err != nil { return handleBuildError(c, err, fullName) } + + if len(config.Labels) == 0 && len(result.Name) > 0 { + config.Labels = map[string]string{"build": result.Name} + } + if err := setLabels(config.Labels, result); err != nil { return err } if err := setAnnotations(map[string]string{newcmd.GeneratedByNamespace: newcmd.GeneratedByNewBuild}, result); err != nil { return err } - if len(output) != 0 && output != "name" { + + indent := " " + switch { + case shortOutput: + indent = "" + case len(output) != 0: return f.Factory.PrintObject(c, result.List, out) + default: + if len(config.Labels) > 0 { + fmt.Fprintf(out, "--> Creating resources with label %s ...\n", labels.SelectorFromSet(config.Labels).String()) + } else { + fmt.Fprintf(out, "--> Creating resources ...\n") + } } - if err := createObjects(f, out, c.Out(), output == "name", true, result); err != nil { + if config.DryRun { + return nil + } + + mapper, _ := f.Object() + if err := createObjects(f, configcmd.NewPrintNameOrErrorAfterIndent(mapper, shortOutput, "created", out, c.Out(), indent), result); err != nil { return err } + if shortOutput { + return nil + } + + fmt.Fprintf(out, "--> Success\n") for _, item := range result.List.Items { switch t := item.(type) { case *buildapi.BuildConfig: - fmt.Fprintf(c.Out(), "Build configuration %q created and build triggered.\n", t.Name) + fmt.Fprintf(out, "%sBuild configuration %q created and build triggered.\n", indent, t.Name) } } if len(result.List.Items) > 0 { - fmt.Fprintf(c.Out(), "Run '%s %s' to check the progress.\n", fullName, StatusRecommendedName) + fmt.Fprintf(out, "%sRun '%s %s' to check the progress.\n", indent, fullName, StatusRecommendedName) } return nil diff --git a/pkg/cmd/cli/describe/helpers.go b/pkg/cmd/cli/describe/helpers.go index 9a8a0f50f2d2..44eff2e616a0 100644 --- a/pkg/cmd/cli/describe/helpers.go +++ b/pkg/cmd/cli/describe/helpers.go @@ -113,6 +113,11 @@ func formatRelativeTime(t time.Time) string { return units.HumanDuration(timeNowFn().Sub(t)) } +// FormatRelativeTime converts a time field into a human readable age string (hours, minutes, days). +func FormatRelativeTime(t time.Time) string { + return formatRelativeTime(t) +} + func formatMeta(out *tabwriter.Writer, m api.ObjectMeta) { formatString(out, "Name", m.Name) if !m.CreationTimestamp.IsZero() { diff --git a/pkg/config/cmd/cmd.go b/pkg/config/cmd/cmd.go index 2c801348e7bb..9a884ffccd7a 100644 --- a/pkg/config/cmd/cmd.go +++ b/pkg/config/cmd/cmd.go @@ -21,11 +21,16 @@ type Bulk struct { } func NewPrintNameOrErrorAfter(mapper meta.RESTMapper, short bool, operation string, out, errs io.Writer) func(*resource.Info, error) { + return NewPrintNameOrErrorAfterIndent(mapper, short, operation, out, errs, "") +} + +func NewPrintNameOrErrorAfterIndent(mapper meta.RESTMapper, short bool, operation string, out, errs io.Writer, indent string) func(*resource.Info, error) { return func(info *resource.Info, err error) { if err == nil { - cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Kind, info.Name, operation) + fmt.Fprintf(out, indent) + cmdutil.PrintSuccess(mapper, short, out, info.Mapping.Kind, info.Name, operation) } else { - fmt.Fprintf(errs, "error: %v\n", err) + fmt.Fprintf(errs, "%serror: %v\n", indent, err) } } } diff --git a/pkg/generate/app/cmd/describe.go b/pkg/generate/app/cmd/describe.go new file mode 100644 index 000000000000..c3dcb4a4595c --- /dev/null +++ b/pkg/generate/app/cmd/describe.go @@ -0,0 +1,209 @@ +package cmd + +import ( + "fmt" + "io" + "sort" + "strings" + + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/sets" + + "github.com/openshift/origin/pkg/cmd/cli/describe" + "github.com/openshift/origin/pkg/generate/app" + imageapi "github.com/openshift/origin/pkg/image/api" + templateapi "github.com/openshift/origin/pkg/template/api" +) + +func localOrRemoteName(meta kapi.ObjectMeta, namespace string) string { + if len(meta.Namespace) == 0 || namespace == meta.Namespace { + return meta.Name + } + return fmt.Sprintf("%s in project %s", meta.Name, meta.Namespace) +} + +func extractFirstImageStreamTag(newOnly bool, images ...*app.ImageRef) string { + for _, image := range images { + if image == nil { + continue + } + if image.Exists() && newOnly { + break + } + ref := image.ObjectReference() + // if the reference is to an IST, the image is intended to be an IST + if ref.Kind != "ImageStreamTag" || !image.AsImageStream { + break + } + return ref.Name + } + return "" +} + +func describeLocatedImage(refInput *app.ComponentInput, baseNamespace string) string { + match := refInput.ResolvedMatch + switch { + case match.ImageStream != nil: + if image := match.Image; image != nil { + shortID := imageapi.ShortDockerImageID(image, 7) + if !image.Created.IsZero() { + shortID = fmt.Sprintf("%s (%s old)", shortID, describe.FormatRelativeTime(image.Created.Time)) + } + return fmt.Sprintf("Found image %s in image stream %q under tag :%s for %q", shortID, localOrRemoteName(match.ImageStream.ObjectMeta, baseNamespace), match.ImageTag, refInput) + } + return fmt.Sprintf("Found tag :%s in image stream %q for %q", match.ImageTag, localOrRemoteName(match.ImageStream.ObjectMeta, baseNamespace), refInput) + case match.Image != nil: + image := match.Image + shortID := imageapi.ShortDockerImageID(image, 7) + if !image.Created.IsZero() { + shortID = fmt.Sprintf("%s (%s old)", shortID, describe.FormatRelativeTime(image.Created.Time)) + } + return fmt.Sprintf("Found Docker image %s from %s for %q", shortID, match.Meta["registry"], refInput) + default: + return "" + } +} + +func describeBuildPipelineWithImage(out io.Writer, ref app.ComponentReference, pipeline *app.Pipeline, baseNamespace string) { + refInput := ref.Input() + match := refInput.ResolvedMatch + + if locatedImage := describeLocatedImage(refInput, baseNamespace); len(locatedImage) > 0 { + fmt.Fprintf(out, "--> %s\n", locatedImage) + } + + trackedImage := extractFirstImageStreamTag(true, pipeline.InputImage, pipeline.Image) + if len(trackedImage) > 0 { + fmt.Fprintf(out, " * An image stream will be created as %q that will track this image\n", trackedImage) + } + if pipeline.Build != nil { + if refInput.Uses != nil && refInput.Uses.Info() != nil { + matches := []string{} + for _, t := range refInput.Uses.Info().Types { + if len(t.Platform) == 0 { + continue + } + if len(t.Version) > 0 { + matches = append(matches, fmt.Sprintf("%s %s", t.Platform, t.Version)) + } + matches = append(matches, t.Platform) + } + if len(matches) > 0 { + fmt.Fprintf(out, " * The source repository appears to match: %s\n", strings.Join(matches, ", ")) + } + } + var strategy string + if pipeline.Build.Strategy.IsDockerBuild { + strategy = "docker" + } else { + strategy = "source" + } + var source string + switch s := pipeline.Build.Source; { + case s.Binary: + source = "binary input" + case len(s.DockerfileContents) > 0: + source = "a predefined Dockerfile" + case s.URL != nil: + source = fmt.Sprintf("source code from %s", s.URL) + default: + source = "" + } + + fmt.Fprintf(out, " * We will perform a %s build using %s\n", strategy, source) + if buildOut, err := pipeline.Build.Output.BuildOutput(); err == nil && buildOut != nil && buildOut.To != nil { + switch to := buildOut.To; { + case to.Kind == "ImageStreamTag": + fmt.Fprintf(out, " * The resulting image will be pushed to image stream %q\n", to.Name) + case to.Kind == "DockerImage": + fmt.Fprintf(out, " * The resulting image will be pushed with Docker to %q\n", to.Name) + default: + fmt.Fprintf(out, " * The resulting image will be pushed to %s %q\n", to.Kind, to.Name) + } + } + if len(trackedImage) > 0 && !pipeline.Build.Source.Binary { + fmt.Fprintf(out, " * Every time %q changes a new build will be triggered\n", trackedImage) + } + } + if pipeline.Deployment != nil { + if len(pipeline.Deployment.Images) > 1 { + fmt.Fprintf(out, " * We will deploy this image as part of deployment config %q\n", pipeline.Deployment.Name) + } else { + fmt.Fprintf(out, " * We will deploy this image in deployment config %q\n", pipeline.Deployment.Name) + } + + if pipeline.Image != nil && pipeline.Image.HasEmptyDir { + fmt.Fprintf(out, " * This image declares volumes and will default to use non-persistent, host-local storage.\n") + fmt.Fprintf(out, " You can add persistent volumes later by running 'volume dc/%s --add ...'\n", pipeline.Deployment.Name) + } + } + if match.Image != nil { + if pipeline.Deployment != nil { + ports := sets.NewString() + if match.Image.Config != nil { + for k := range match.Image.Config.ExposedPorts { + ports.Insert(k) + } + } + switch len(ports) { + case 0: + fmt.Fprintf(out, " * The image does not expose any ports - if you want to load balance or send traffic to this component\n") + fmt.Fprintf(out, " you will need to create a service with 'expose dc/%s --port=[port]' later\n", pipeline.Deployment.Name) + default: + orderedPorts := ports.List() + sort.Sort(sort.StringSlice(orderedPorts)) + if len(orderedPorts) == 1 { + fmt.Fprintf(out, " * Port %s will be load balanced by service %q\n", orderedPorts[0], pipeline.Deployment.Name) + } else { + fmt.Fprintf(out, " * Ports %s will be load balanced by service %q\n", strings.Join(orderedPorts, ", "), pipeline.Deployment.Name) + } + } + } + } +} + +func describeGeneratedTemplate(out io.Writer, ref app.ComponentReference, result *templateapi.Template, baseNamespace string) { + fmt.Fprintf(out, "--> Deploying template %s for %q\n", localOrRemoteName(ref.Input().ResolvedMatch.Template.ObjectMeta, baseNamespace), ref.Input()) + if len(result.Parameters) > 0 { + fmt.Fprintf(out, " With parameters:\n") + for _, p := range result.Parameters { + name := p.DisplayName + if len(name) == 0 { + name = p.Name + } + var generated string + if len(p.Generate) > 0 { + generated = " # generated" + } + fmt.Fprintf(out, " %s=%s%s\n", name, p.Value, generated) + } + } +} + +func describeGeneratedJob(out io.Writer, ref app.ComponentReference, pod *kapi.Pod, secret *kapi.Secret, baseNamespace string) { + refInput := ref.Input() + generatorInput := refInput.ResolvedMatch.GeneratorInput + hasToken := generatorInput.Token != nil + + fmt.Fprintf(out, "--> Installing application from %q\n", refInput) + if locatedImage := describeLocatedImage(refInput, baseNamespace); len(locatedImage) > 0 { + fmt.Fprintf(out, " * %s\n", locatedImage) + } + + fmt.Fprintf(out, " * Install will run in pod %q\n", localOrRemoteName(pod.ObjectMeta, baseNamespace)) + switch { + case secret != nil: + fmt.Fprintf(out, " * The pod has access to your current session token through the secret %q\n", localOrRemoteName(secret.ObjectMeta, baseNamespace)) + fmt.Fprintf(out, " * If you cancel the install, you should delete the secret or log out of your session\n") + case hasToken && generatorInput.Token.Env != nil: + fmt.Fprintf(out, " * The pod has access to your current session token via environment variable %s\n", *generatorInput.Token.Env) + fmt.Fprintf(out, " * If you cancel the install, you should delete the pod or log out of your session\n") + case hasToken: + fmt.Fprintf(out, " * The pod has access to your current session token. Please delete the pod if you cancel the install\n") + } + if hasToken { + fmt.Fprintf(out, "--> WARNING: The pod requires access to your current session token to install this image. Only\n") + fmt.Fprintf(out, " grant access to images whose source you trust. The image will be able to perform any\n") + fmt.Fprintf(out, " action you can take on the cluster.\n") + } +} diff --git a/pkg/generate/app/cmd/newapp.go b/pkg/generate/app/cmd/newapp.go index 11b7113e6a71..9b129973f62e 100644 --- a/pkg/generate/app/cmd/newapp.go +++ b/pkg/generate/app/cmd/newapp.go @@ -13,12 +13,10 @@ import ( "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" - "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/errors" kvalidation "k8s.io/kubernetes/pkg/util/validation" - buildapi "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/client" cmdutil "github.com/openshift/origin/pkg/cmd/util" "github.com/openshift/origin/pkg/dockerregistry" @@ -72,6 +70,7 @@ type AppConfig struct { ExpectToBuild bool BinaryBuild bool AllowMissingImages bool + Deploy bool SkipGeneration bool AllowGenerationErrors bool @@ -81,6 +80,7 @@ type AppConfig struct { AsSearch bool AsList bool + DryRun bool Out io.Writer ErrOut io.Writer @@ -338,14 +338,6 @@ func (c *AppConfig) validate() (app.ComponentReferences, app.SourceRepositories, b.AddGroups(c.Groups) refs, repos, errs := b.Result() - if len(c.ContextDir) > 0 && len(repos) > 0 { - repos[0].SetContextDir(c.ContextDir) - if len(repos) > 1 { - glog.Warningf("You have specified more than one source repository and a context directory. "+ - "The context directory will be applied to the first repository: %q", repos[0]) - } - } - if len(c.Strategy) != 0 && len(repos) == 0 { errs = append(errs, fmt.Errorf("when --strategy is specified you must provide at least one source code location")) } @@ -614,20 +606,24 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme glog.V(4).Infof("found group: %#v", group) common := app.PipelineGroup{} for _, ref := range group { + refInput := ref.Input() + var pipeline *app.Pipeline var name string - if ref.Input().ExpectToBuild { - glog.V(4).Infof("will use %q as the base image for a source build of %q", ref, ref.Input().Uses) - input, err := app.InputImageFromMatch(ref.Input().ResolvedMatch) + + if refInput.ExpectToBuild { + glog.V(4).Infof("will use %q as the base image for a source build of %q", ref, refInput.Uses) + input, err := app.InputImageFromMatch(refInput.ResolvedMatch) if err != nil { - return nil, fmt.Errorf("can't build %q: %v", ref.Input(), err) + return nil, fmt.Errorf("can't build %q: %v", refInput, err) } if !input.AsImageStream { - glog.Warningf("Could not find an image stream match for %q. Make sure that a Docker image with that tag is available on the node for the build to succeed.", ref.Input().ResolvedMatch.Value) + glog.Warningf("Could not find an image stream match for %q. Make sure that a Docker image with that tag is available on the node for the build to succeed.", refInput.ResolvedMatch.Value) } - strategy, source, err := app.StrategyAndSourceForRepository(ref.Input().Uses, input) + + strategy, source, err := app.StrategyAndSourceForRepository(refInput.Uses, input) if err != nil { - return nil, fmt.Errorf("can't build %q: %v", ref.Input(), err) + return nil, fmt.Errorf("can't build %q: %v", refInput, err) } // Override resource names from the cli @@ -646,8 +642,8 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme } // Append any exposed ports from Dockerfile to input image - if ref.Input().Uses.IsDockerBuild() && ref.Input().Uses.Info() != nil { - node := ref.Input().Uses.Info().Dockerfile.AST() + if refInput.Uses.IsDockerBuild() && refInput.Uses.Info() != nil { + node := refInput.Uses.Info().Dockerfile.AST() ports := dockerfileutil.LastExposedPorts(node) if len(ports) > 0 { if input.Info == nil { @@ -661,15 +657,15 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme } } } - if pipeline, err = app.NewBuildPipeline(ref.Input().String(), input, c.OutputDocker, strategy, c.GetBuildEnvironment(environment), source); err != nil { - return nil, fmt.Errorf("can't build %q: %v", ref.Input(), err) + if pipeline, err = app.NewBuildPipeline(refInput.String(), input, c.OutputDocker, strategy, c.GetBuildEnvironment(environment), source); err != nil { + return nil, fmt.Errorf("can't build %q: %v", refInput, err) } } else { glog.V(4).Infof("will include %q", ref) - input, err := app.InputImageFromMatch(ref.Input().ResolvedMatch) + input, err := app.InputImageFromMatch(refInput.ResolvedMatch) if err != nil { - return nil, fmt.Errorf("can't include %q: %v", ref.Input(), err) + return nil, fmt.Errorf("can't include %q: %v", refInput, err) } name = c.Name if len(name) == 0 { @@ -684,20 +680,25 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme return nil, err } input.ObjectName = name - if pipeline, err = app.NewImagePipeline(ref.Input().String(), input); err != nil { - return nil, fmt.Errorf("can't include %q: %v", ref.Input(), err) + if pipeline, err = app.NewImagePipeline(refInput.String(), input); err != nil { + return nil, fmt.Errorf("can't include %q: %v", refInput, err) } } - if err := pipeline.NeedsDeployment(environment, c.Labels, name); err != nil { - return nil, fmt.Errorf("can't set up a deployment for %q: %v", ref.Input(), err) + if c.Deploy { + if err := pipeline.NeedsDeployment(environment, c.Labels, name); err != nil { + return nil, fmt.Errorf("can't set up a deployment for %q: %v", refInput, err) + } } common = append(common, pipeline) - } - if err := common.Reduce(); err != nil { - return nil, fmt.Errorf("can't create a pipeline from %s: %v", common, err) + if err := common.Reduce(); err != nil { + return nil, fmt.Errorf("can't create a pipeline from %s: %v", common, err) + } + + describeBuildPipelineWithImage(c.Out, ref, pipeline, c.originNamespace) } + pipelines = append(pipelines, common...) } return pipelines, nil @@ -732,10 +733,21 @@ func (c *AppConfig) buildTemplates(components app.ComponentReferences, environme return nil, fmt.Errorf("error processing template %s/%s: %v", c.originNamespace, tpl.Name, errs) } objects = append(objects, result.Objects...) + + describeGeneratedTemplate(c.Out, ref, result, c.originNamespace) } return objects, nil } +// fakeSecretAccessor is used during dry runs of installation +type fakeSecretAccessor struct { + token string +} + +func (a *fakeSecretAccessor) Token() (string, error) { + return a.token, nil +} + // installComponents attempts to create pods to run installable images identified by the user. If an image // is installable, we check whether it requires access to the user token. If so, the caller must have // explicitly granted that access (because the token may be the user's). @@ -777,12 +789,16 @@ func (c *AppConfig) installComponents(components app.ComponentReferences) ([]run imageRef.ObjectName = name glog.V(4).Infof("Proposed installable image %#v", imageRef) + secretAccessor := c.SecretAccessor generatorInput := input.ResolvedMatch.GeneratorInput - if generatorInput.Token != nil && !c.AllowSecretUse || c.SecretAccessor == nil { - return nil, "", ErrRequiresExplicitAccess{*input.ResolvedMatch} + if generatorInput.Token != nil && !c.AllowSecretUse || secretAccessor == nil { + if !c.DryRun { + return nil, "", ErrRequiresExplicitAccess{*input.ResolvedMatch} + } + secretAccessor = &fakeSecretAccessor{token: "FAKE_TOKEN"} } - pod, secret, err := imageRef.InstallablePod(generatorInput, c.SecretAccessor) + pod, secret, err := imageRef.InstallablePod(generatorInput, secretAccessor) if err != nil { return nil, "", err } @@ -797,72 +813,16 @@ func (c *AppConfig) installComponents(components app.ComponentReferences) ([]run }) } + describeGeneratedJob(c.Out, job, pod, secret, c.originNamespace) + return objects, name, nil } -// RunAll executes the provided config to generate all objects. -func (c *AppConfig) RunAll() (*AppResult, error) { +// Run executes the provided config to generate objects. +func (c *AppConfig) Run() (*AppResult, error) { return c.run(app.Acceptors{app.NewAcceptUnique(c.typer), app.AcceptNew}) } -// RunBuilds executes the provided config to generate just builds. -func (c *AppConfig) RunBuilds() (*AppResult, error) { - bcAcceptor := app.NewAcceptBuildConfigs(c.typer) - result, err := c.run(app.Acceptors{bcAcceptor, app.NewAcceptUnique(c.typer), app.AcceptNew}) - if err != nil { - return nil, err - } - return filterImageStreams(result), nil -} - -func filterImageStreams(result *AppResult) *AppResult { - // 1st pass to get images from all BuildConfigs - imageStreams := map[string]bool{} - for _, item := range result.List.Items { - if bc, ok := item.(*buildapi.BuildConfig); ok { - to := bc.Spec.Output.To - if to != nil && to.Kind == "ImageStreamTag" { - imageStreams[makeImageStreamKey(*to)] = true - } - switch bc.Spec.Strategy.Type { - case buildapi.DockerBuildStrategyType: - from := bc.Spec.Strategy.DockerStrategy.From - if from != nil && from.Kind == "ImageStreamTag" { - imageStreams[makeImageStreamKey(*from)] = true - } - case buildapi.SourceBuildStrategyType: - from := bc.Spec.Strategy.SourceStrategy.From - if from.Kind == "ImageStreamTag" { - imageStreams[makeImageStreamKey(from)] = true - } - case buildapi.CustomBuildStrategyType: - from := bc.Spec.Strategy.CustomStrategy.From - if from.Kind == "ImageStreamTag" { - imageStreams[makeImageStreamKey(from)] = true - } - } - } - } - items := []runtime.Object{} - // 2nd pass to remove ImageStreams not used by BuildConfigs - for _, item := range result.List.Items { - if is, ok := item.(*imageapi.ImageStream); ok { - if _, ok := imageStreams[types.NamespacedName{Namespace: is.Namespace, Name: is.Name}.String()]; ok { - items = append(items, is) - } - } else { - items = append(items, item) - } - } - result.List.Items = items - return result -} - -func makeImageStreamKey(ref kapi.ObjectReference) string { - name, _, _ := imageapi.SplitImageStreamTag(ref.Name) - return types.NamespacedName{Namespace: ref.Namespace, Name: name}.String() -} - // RunQuery executes the provided config and returns the result of the resolution. func (c *AppConfig) RunQuery() (*QueryResult, error) { c.ensureDockerSearcher() @@ -1015,19 +975,11 @@ func (c *AppConfig) run(acceptors app.Acceptors) (*AppResult, error) { objects := app.Objects{} accept := app.NewAcceptFirst() - warned := make(map[string]struct{}) for _, p := range pipelines { accepted, err := p.Objects(accept, acceptors) if err != nil { return nil, fmt.Errorf("can't setup %q: %v", p.From, err) } - if p.Image != nil && p.Image.HasEmptyDir { - spec := p.Image.PullSpec() - if _, ok := warned[spec]; ok { - fmt.Fprintf(c.ErrOut, "WARNING: Image %q uses an empty directory volume. Data in these volumes is not persisted across deployments.\n", p.Image.Reference.Name) - warned[spec] = struct{}{} - } - } objects = append(objects, accepted...) } diff --git a/pkg/generate/app/componentref.go b/pkg/generate/app/componentref.go index 251e6c6bc3a1..324fe7e50cad 100644 --- a/pkg/generate/app/componentref.go +++ b/pkg/generate/app/componentref.go @@ -256,10 +256,11 @@ func NewComponentInput(input string) (*ComponentInput, string, error) { // ComponentInput is the necessary input for creating a component type ComponentInput struct { - GroupID int - From string - Argument string - Value string + GroupID int + From string + Argument string + Value string + ExpectToBuild bool Uses *SourceRepository diff --git a/pkg/generate/app/dockerimagelookup.go b/pkg/generate/app/dockerimagelookup.go index c02beb8f26a2..88088273f8cf 100644 --- a/pkg/generate/app/dockerimagelookup.go +++ b/pkg/generate/app/dockerimagelookup.go @@ -221,10 +221,7 @@ func (r DockerRegistrySearcher) Search(terms ...string) (ComponentMatches, error } func descriptionFor(image *imageapi.DockerImage, value, from string, tag string) string { - shortID := image.ID - if len(shortID) > 7 { - shortID = shortID[:7] - } + shortID := imageapi.ShortDockerImageID(image, 7) tagPart := "" if len(tag) > 0 { tagPart = fmt.Sprintf(" (tag %q)", tag) diff --git a/pkg/generate/app/imageref.go b/pkg/generate/app/imageref.go index dac2b8ec78aa..fb0899bcf6d1 100644 --- a/pkg/generate/app/imageref.go +++ b/pkg/generate/app/imageref.go @@ -144,10 +144,16 @@ type ImageRef struct { // but does not affect the DockerImageReference ObjectName string + // This should *only* be set if the image stream already exists Stream *imageapi.ImageStream Info *imageapi.DockerImage } +// Exists returns true if the image stream exists +func (r *ImageRef) Exists() bool { + return r.Stream != nil +} + // ObjectReference returns an object reference from the image reference func (r *ImageRef) ObjectReference() kapi.ObjectReference { switch { diff --git a/pkg/generate/app/pipeline.go b/pkg/generate/app/pipeline.go index 8958f53236ee..bb0e0cb98533 100644 --- a/pkg/generate/app/pipeline.go +++ b/pkg/generate/app/pipeline.go @@ -6,7 +6,6 @@ import ( "sort" "strings" - "github.com/golang/glog" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/runtime" @@ -241,7 +240,6 @@ func AddServices(objects Objects, firstPortOnly bool) Objects { } } if len(svc.Spec.Ports) == 0 { - glog.Warningf("A service will not be generated for DeploymentConfig %q because no exposed ports were detected. Use 'oc expose dc %q --port=[port]' to create a service.", t.Name, t.Name) continue } svcs = append(svcs, svc) diff --git a/pkg/image/api/helper.go b/pkg/image/api/helper.go index ab129d6577e1..455db8418f53 100644 --- a/pkg/image/api/helper.go +++ b/pkg/image/api/helper.go @@ -444,3 +444,15 @@ func ResolveImageID(stream *ImageStream, imageID string) sets.String { } return set } + +// ShortDockerImageID returns a short form of the provided DockerImage ID for display +func ShortDockerImageID(image *DockerImage, length int) string { + id := image.ID + if s, err := digest.ParseDigest(id); err == nil { + id = s.Hex() + } + if len(id) > length { + id = id[:length] + } + return id +} diff --git a/test/cmd/newapp.sh b/test/cmd/newapp.sh index 8bbca898a360..68032265e815 100755 --- a/test/cmd/newapp.sh +++ b/test/cmd/newapp.sh @@ -98,7 +98,7 @@ oc create -f test/fixtures/installable-stream.yaml project=$(oc project -q) oc policy add-role-to-user edit test-user oc login -u test-user -p anything -oc project "${project}" +tryuntil oc project "${project}" tryuntil oc get imagestreamtags installable:file tryuntil oc get imagestreamtags installable:token