diff --git a/pkg/cmd/cli/cli.go b/pkg/cmd/cli/cli.go index 38fae54e0eba..e055e10def90 100644 --- a/pkg/cmd/cli/cli.go +++ b/pkg/cmd/cli/cli.go @@ -69,7 +69,7 @@ func NewCommandCLI(name, fullName string) *cobra.Command { cmds.AddCommand(f.NewCmdProxy(out)) // Origin commands - // cmds.AddCommand(cmd.NewCmdNewApplication(f, out)) + cmds.AddCommand(cmd.NewCmdNewApplication(f, out)) cmds.AddCommand(cmd.NewCmdProcess(f, out)) // Origin build commands diff --git a/pkg/cmd/cli/cmd/newapp.go b/pkg/cmd/cli/cmd/newapp.go index 6ee706283749..908e37d20ca3 100644 --- a/pkg/cmd/cli/cmd/newapp.go +++ b/pkg/cmd/cli/cmd/newapp.go @@ -1,8 +1,12 @@ package cmd import ( + "fmt" "io" + "os" + kcmd "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/golang/glog" "github.com/spf13/cobra" @@ -18,22 +22,35 @@ type usage interface { const longNewAppDescription = ` Create a new application in OpenShift by specifying source code, templates, and/or images. +This command will try to build up the components of an application using images or code +located on your system. It will lookup the images on the local Docker installation (if +available), a Docker registry, or an OpenShift image repository. If you specify a source +code URL, it will set up a build that takes your source code and converts it into an +image that can run inside of a pod. The images will be deployed via a deployment +configuration, and a service will be hookup up to the first public port of the app. + Examples: + $ osc new-app . + + + $ osc new-app mysql + - $ osc new-app . - + $ osc new-app myregistry.com/mycompany/mysql + - $ osc new-app mysql - + $ osc new-app openshift/ruby-20-centos~git@github.com/mfojtik/sinatra-app-example + - $ osc new-app myregistry.com/mycompany/mysql - +If you specify source code, you may need to run a build with 'start-build' after the +application is created. - $ osc new-app openshift/ruby-20-centos~git@github.com/mfojtik/sinatra-app-example - ` +ALPHA: This command is under active development - feedback is appreciated. +` func NewCmdNewApplication(f *Factory, out io.Writer) *cobra.Command { config := newcmd.NewAppConfig() + helper := dockerutil.NewHelper() cmd := &cobra.Command{ Use: "new-app [--code=]", @@ -41,31 +58,81 @@ func NewCmdNewApplication(f *Factory, out io.Writer) *cobra.Command { Long: longNewAppDescription, Run: func(c *cobra.Command, args []string) { + namespace, err := f.DefaultNamespace(c) + checkErr(err) + if dockerClient, _, err := helper.GetClient(); err == nil { - config.SetDockerClient(dockerClient) + if err := dockerClient.Ping(); err == nil { + config.SetDockerClient(dockerClient) + } else { + glog.V(2).Infof("No local Docker daemon detected: %v", err) + } } - if osclient, _, err := f.Clients(c); err == nil { - namespace, err := f.DefaultNamespace(c) - checkErr(err) - config.SetOpenShiftClient(osclient, namespace) - } else { - glog.Warningf("error getting client: %v", err) + + osclient, _, err := f.Clients(c) + if err != nil { + glog.Fatalf("Error getting client: %v", err) } + config.SetOpenShiftClient(osclient, namespace) + unknown := config.AddArguments(args) if len(unknown) != 0 { glog.Fatalf("Did not recognize the following arguments: %v", unknown) } - if err := config.Run(out, c.Help); err != nil { + + obj, err := config.Run(out) + if err != nil { if errs, ok := err.(errors.Aggregate); ok { if len(errs.Errors()) == 1 { err = errs.Errors()[0] } } + if err == newcmd.ErrNoInputs { + // TODO: suggest things to the user + glog.Fatal("You must specify one or more images, image repositories, or source code locations to create an application.") + } if u, ok := err.(usage); ok { glog.Fatal(u.UsageError(c.CommandPath())) } glog.Fatalf("Error: %v", err) } + + if len(kcmd.GetFlagString(c, "output")) != 0 { + if err := kcmd.PrintObject(c, obj.List, f.Factory, out); err != nil { + glog.Fatalf("Error: %v", err) + } + return + } + + mapper, typer := f.Object(c) + resourceMapper := &resource.Mapper{typer, mapper, kcmd.ClientMapperForCommand(c, f.Factory)} + errs := []error{} + for _, item := range obj.List.Items { + info, err := resourceMapper.InfoForObject(item) + if err != nil { + errs = append(errs, err) + continue + } + data, err := info.Mapping.Codec.Encode(item) + if err != nil { + errs = append(errs, err) + glog.Error(err) + continue + } + if err := resource.NewHelper(info.Client, info.Mapping).Create(namespace, false, data); err != nil { + errs = append(errs, err) + glog.Error(err) + continue + } + fmt.Fprintf(out, "%s\n", info.Name) + } + if len(errs) != 0 { + os.Exit(1) + } + + for _, s := range obj.BuildNames { + fmt.Fprintf(os.Stderr, "A build was created - run `osc start-build %s` to start it\n", s) + } }, } @@ -75,5 +142,8 @@ func NewCmdNewApplication(f *Factory, out io.Writer) *cobra.Command { cmd.Flags().Var(&config.Groups, "group", "Indicate components that should be grouped together as +.") cmd.Flags().VarP(&config.Environment, "env", "e", "Specify key value pairs of environment variables to set into each container.") cmd.Flags().StringVar(&config.TypeOfBuild, "build", "", "Specify the type of build to use if you don't want to detect (docker|source)") + + kcmd.AddPrinterFlags(cmd) + return cmd } diff --git a/pkg/cmd/experimental/generate/generate.go b/pkg/cmd/experimental/generate/generate.go index 32136e830bab..42a7c3ee8cf8 100644 --- a/pkg/cmd/experimental/generate/generate.go +++ b/pkg/cmd/experimental/generate/generate.go @@ -147,7 +147,7 @@ func newImageResolver(namespace string, osClient osclient.Interface, dockerClien resolver := genapp.PerfectMatchWeightedResolver{} if dockerClient != nil { - localDockerResolver := &genapp.DockerClientResolver{dockerClient} + localDockerResolver := &genapp.DockerClientResolver{Client: dockerClient} resolver = append(resolver, genapp.WeightedResolver{localDockerResolver, 0.0}) } diff --git a/pkg/cmd/openshift/openshift.go b/pkg/cmd/openshift/openshift.go index e233ace3b176..472072fb3e89 100644 --- a/pkg/cmd/openshift/openshift.go +++ b/pkg/cmd/openshift/openshift.go @@ -32,11 +32,7 @@ OpenShift is built around Docker and the Kubernetes cluster container manager. Docker installed on this machine to start your server. Note: This is an alpha release of OpenShift and will change significantly. See - - https://github.com/openshift/origin - -for the latest information on OpenShift. - + https://github.com/openshift/origin for the latest information on OpenShift. ` // CommandFor returns the appropriate command for this base name, diff --git a/pkg/generate/app/cmd/newapp.go b/pkg/generate/app/cmd/newapp.go index f8160a330dc2..fd99014b2e80 100644 --- a/pkg/generate/app/cmd/newapp.go +++ b/pkg/generate/app/cmd/newapp.go @@ -6,12 +6,12 @@ import ( "strings" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/fsouza/go-dockerclient" "github.com/golang/glog" + 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" @@ -31,9 +31,8 @@ type AppConfig struct { TypeOfBuild string - localDockerResolver app.Resolver - dockerRegistryResolver app.Resolver - imageStreamResolver app.Resolver + dockerResolver app.Resolver + imageStreamResolver app.Resolver searcher app.Searcher detector app.Detector @@ -50,17 +49,20 @@ type errlist interface { func NewAppConfig() *AppConfig { return &AppConfig{ - searcher: &mockSearcher{}, detector: app.SourceRepositoryEnumerator{ Detectors: source.DefaultDetectors, Tester: dockerfile.NewTester(), }, - dockerRegistryResolver: app.DockerRegistryResolver{dockerregistry.NewClient()}, + dockerResolver: app.DockerRegistryResolver{dockerregistry.NewClient()}, } } func (c *AppConfig) SetDockerClient(dockerclient *docker.Client) { - c.localDockerResolver = app.DockerClientResolver{dockerclient} + c.dockerResolver = app.DockerClientResolver{ + Client: dockerclient, + + RegistryResolver: c.dockerResolver, + } } func (c *AppConfig) SetOpenShiftClient(osclient client.Interface, originNamespace string) { @@ -100,7 +102,7 @@ func (c *AppConfig) validate() (app.ComponentReferences, []*app.SourceRepository } b.AddImages(c.DockerImages, func(input *app.ComponentInput) app.ComponentReference { input.Argument = fmt.Sprintf("--docker-image=%q", input.From) - input.Resolver = c.dockerRegistryResolver + input.Resolver = c.dockerResolver return input }) b.AddImages(c.ImageStreams, func(input *app.ComponentInput) app.ComponentReference { @@ -111,8 +113,7 @@ func (c *AppConfig) validate() (app.ComponentReferences, []*app.SourceRepository b.AddImages(c.Components, func(input *app.ComponentInput) app.ComponentReference { input.Resolver = app.PerfectMatchWeightedResolver{ app.WeightedResolver{Resolver: c.imageStreamResolver, Weight: 0.0}, - app.WeightedResolver{Resolver: c.dockerRegistryResolver, Weight: 0.0}, - app.WeightedResolver{Resolver: c.localDockerResolver, Weight: 0.0}, + app.WeightedResolver{Resolver: c.dockerResolver, Weight: 0.0}, } return input }) @@ -280,39 +281,46 @@ func (c *AppConfig) buildPipelines(components app.ComponentReferences, environme return pipelines, nil } +var ErrNoInputs = fmt.Errorf("no inputs provided") + +type AppResult struct { + List *kapi.List + + BuildNames []string + HasSource bool +} + // Run executes the provided config. -func (c *AppConfig) Run(out io.Writer, helpFn func() error) error { +func (c *AppConfig) Run(out io.Writer) (*AppResult, error) { components, repositories, environment, err := c.validate() if err != nil { - return err + return nil, err } hasSource := len(repositories) != 0 hasImages := len(components) != 0 if !hasSource && !hasImages { - // display help page - // TODO: return usage error, which should trigger help display - return helpFn() + return nil, ErrNoInputs } if err := c.resolve(components); err != nil { - return err + return nil, err } if err := c.ensureHasSource(components, repositories); err != nil { - return err + return nil, err } glog.V(4).Infof("Code %v", repositories) glog.V(4).Infof("Images %v", components) if err := c.detectSource(repositories); err != nil { - return err + return nil, err } pipelines, err := c.buildPipelines(components, app.Environment(environment)) if err != nil { - return err + return nil, err } objects := app.Objects{} @@ -320,19 +328,27 @@ func (c *AppConfig) Run(out io.Writer, helpFn func() error) error { for _, p := range pipelines { obj, err := p.Objects(accept) if err != nil { - return fmt.Errorf("can't setup %q: %v", p.From, err) + return nil, fmt.Errorf("can't setup %q: %v", p.From, err) } objects = append(objects, obj...) } objects = app.AddServices(objects) - list := &kapi.List{Items: objects} - p, _, err := kubectl.GetPrinter("yaml", "") - if err != nil { - return err + buildNames := []string{} + for _, obj := range objects { + switch t := obj.(type) { + case *buildapi.BuildConfig: + buildNames = append(buildNames, t.Name) + } } - return p.PrintObj(list, out) + + list := &kapi.List{Items: objects} + return &AppResult{ + List: list, + BuildNames: buildNames, + HasSource: hasSource, + }, nil } type mockSearcher struct{} diff --git a/pkg/generate/app/componentref.go b/pkg/generate/app/componentref.go index bcbf2b0b7774..7df04269d167 100644 --- a/pkg/generate/app/componentref.go +++ b/pkg/generate/app/componentref.go @@ -119,6 +119,16 @@ func (m ScoredComponentMatches) Len() int { return len(m) } func (m ScoredComponentMatches) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (m ScoredComponentMatches) Less(i, j int) bool { return m[i].Score < m[j].Score } +func (m ScoredComponentMatches) Exact() []*ComponentMatch { + out := []*ComponentMatch{} + for _, match := range m { + if match.Score == 0.0 { + out = append(out, match) + } + } + return out +} + type WeightedResolver struct { Resolver Weight float32 diff --git a/pkg/generate/app/errors.go b/pkg/generate/app/errors.go index a1a1a124ee72..c550f9e2a1c8 100644 --- a/pkg/generate/app/errors.go +++ b/pkg/generate/app/errors.go @@ -18,12 +18,14 @@ func (e ErrNoMatch) Error() string { } func (e ErrNoMatch) UsageError(commandName string) string { - return fmt.Sprintf(` -%[3]s - you can try to search for images or templates that may match this name with: + return fmt.Sprintf("%[3]s - does a Docker image with that name exist?", e.value, commandName, e.Error()) + + /*` + %[3]s - you can try to search for images or templates that may match this name with: - $ %[2]s -S %[1]q + $ %[2]s -S %[1]q -`, e.value, commandName, e.Error()) + `*/ } type ErrMultipleMatches struct { @@ -42,7 +44,7 @@ func (e ErrMultipleMatches) UsageError(commandName string) string { fmt.Fprintf(buf, " %s\n\n", match.Description) } return fmt.Sprintf(` -The argument %[1]q could apply to the following images or templates: +The argument %[1]q could apply to the following images or image repositories: %[2]s `, e.image, buf.String()) diff --git a/pkg/generate/app/imagelookup.go b/pkg/generate/app/imagelookup.go index 80b589210e05..d5e6f558024a 100644 --- a/pkg/generate/app/imagelookup.go +++ b/pkg/generate/app/imagelookup.go @@ -2,11 +2,14 @@ package app import ( "fmt" + "sort" + "strings" "github.com/fsouza/go-dockerclient" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/golang/glog" "github.com/openshift/origin/pkg/client" @@ -16,23 +19,102 @@ import ( type DockerClientResolver struct { Client *docker.Client + + // Optional, will delegate resolution to the registry if no local + // exact matches are found. + RegistryResolver Resolver } func (r DockerClientResolver) Resolve(value string) (*ComponentMatch, error) { - image, err := r.Client.InspectImage(value) - switch { - case err == docker.ErrNoSuchImage: + registry, namespace, name, tag, err := imageapi.SplitDockerPullSpec(value) + if err != nil { + return nil, err + } + + glog.V(4).Infof("checking local Docker daemon %s/%s/%s with tag %q", registry, namespace, name, tag) + images, err := r.Client.ListImages(docker.ListImagesOptions{}) + if err != nil { + return nil, err + } + matches := ScoredComponentMatches{} + for _, image := range images { + if tags := matchTag(image, value, registry, namespace, name, tag); len(tags) > 0 { + matches = append(matches, tags...) + } + } + sort.Sort(matches) + if exact := matches.Exact(); len(exact) > 0 { + matches = exact + } else { + if r.RegistryResolver != nil { + match, err := r.RegistryResolver.Resolve(value) + switch err.(type) { + case nil: + return match, nil + case ErrNoMatch: + // show our partial matches + case ErrMultipleMatches: + // TODO: add these matches to our local results + return nil, err + default: + return nil, err + } + } + } + + errs := []error{} + for i, match := range matches { + if match.Image != nil { + continue + } + updated, err := r.lookup(match.Value) + if err != nil { + errs = append(errs, err) + continue + } + updated.Score = match.Score + updated.ImageTag = tag + matches[i] = updated + } + + if len(errs) != 0 { + if len(errs) == 1 { + err := errs[0] + if err == docker.ErrNoSuchImage { + return nil, ErrNoMatch{value: value} + } + return nil, err + } + return nil, utilerrors.NewAggregate(errs) + } + + switch len(matches) { + case 0: return nil, ErrNoMatch{value: value} - case err != nil: + case 1: + return matches[0], nil + default: + return nil, ErrMultipleMatches{image: value, matches: matches} + } +} + +func (r DockerClientResolver) lookup(value string) (*ComponentMatch, error) { + image, err := r.Client.InspectImage(value) + if err != nil { + return nil, err + } + dockerImage := &imageapi.DockerImage{} + if err := kapi.Scheme.Convert(image, dockerImage); err != nil { return nil, err } return &ComponentMatch{ Value: value, Argument: fmt.Sprintf("--docker-image=%q", value), Name: value, - Description: fmt.Sprintf("Docker image %q by %s\n%s", value, image.Author, image.Comment), - Builder: false, - Score: 0, + Description: descriptionFor(dockerImage, value, "local Docker"), + Builder: IsBuilderImage(dockerImage), + Score: 0.0, + Image: dockerImage, }, nil } @@ -45,6 +127,7 @@ func (r DockerRegistryResolver) Resolve(value string) (*ComponentMatch, error) { if err != nil { return nil, err } + glog.V(4).Infof("checking Docker registry %s/%s/%s with tag %q", registry, namespace, name, tag) connection, err := r.Client.Connect(registry) if err != nil { if dockerregistry.IsRegistryNotFound(err) { @@ -67,11 +150,16 @@ func (r DockerRegistryResolver) Resolve(value string) (*ComponentMatch, error) { if err = kapi.Scheme.Convert(image, dockerImage); err != nil { return nil, err } + + from := registry + if len(registry) == 0 { + registry = "DockerHub" + } return &ComponentMatch{ Value: value, Argument: fmt.Sprintf("--docker-image=%q", value), Name: value, - Description: fmt.Sprintf("Docker image %q (%q)", value, image.ID), + Description: descriptionFor(dockerImage, value, from), Builder: IsBuilderImage(dockerImage), Score: 0, Image: dockerImage, @@ -79,6 +167,85 @@ func (r DockerRegistryResolver) Resolve(value string) (*ComponentMatch, error) { }, nil } +func descriptionFor(image *imageapi.DockerImage, value, from string) string { + shortID := image.ID + if len(shortID) > 7 { + shortID = shortID[:7] + } + parts := []string{fmt.Sprintf("Docker image %q", value), shortID, fmt.Sprintf("from %s", from)} + if image.Size > 0 { + mb := float64(image.Size) / float64(1024*1024) + parts = append(parts, fmt.Sprintf("%f", mb)) + } + if len(image.Author) > 0 { + parts = append(parts, fmt.Sprintf("author %s", image.Author)) + } + if len(image.Comment) > 0 { + parts = append(parts, image.Comment) + } + return strings.Join(parts, ", ") +} + +func partialScorer(a, b string, prefix bool, partial, none float32) (bool, float32) { + switch { + case len(a) == 0 && len(b) != 0, len(a) != 0 && len(b) == 0: + return true, partial + case a != b: + if prefix { + if strings.HasPrefix(a, b) || strings.HasPrefix(b, a) { + return true, partial + } + } + return false, none + default: + return true, 0.0 + } +} + +func matchTag(image docker.APIImages, value, registry, namespace, name, tag string) []*ComponentMatch { + if len(tag) == 0 { + tag = "latest" + } + matches := []*ComponentMatch{} + for _, s := range image.RepoTags { + if value == s { + matches = append(matches, &ComponentMatch{ + Value: s, + Score: 0.0, + }) + continue + } + iRegistry, iNamespace, iName, iTag, err := imageapi.SplitDockerPullSpec(s) + if err != nil { + continue + } + if len(iTag) == 0 { + iTag = "latest" + } + match := &ComponentMatch{} + ok, score := partialScorer(name, iName, true, 0.5, 1.0) + if !ok { + continue + } + match.Score += score + _, score = partialScorer(namespace, iNamespace, false, 0.5, 1.0) + match.Score += score + _, score = partialScorer(registry, iRegistry, false, 0.5, 1.0) + match.Score += score + _, score = partialScorer(tag, iTag, false, 0.5, 1.0) + match.Score += score + + if match.Score >= 4.0 { + continue + } + match.Score = match.Score / 4.0 + glog.V(4).Infof("partial match on %q with %f", s, match.Score) + match.Value = s + matches = append(matches, match) + } + return matches +} + type ImageStreamResolver struct { Client client.ImageRepositoriesNamespacer Images client.ImagesNamespacer diff --git a/pkg/generate/app/pipeline.go b/pkg/generate/app/pipeline.go index 28f9d6347696..c5f02c72b904 100644 --- a/pkg/generate/app/pipeline.go +++ b/pkg/generate/app/pipeline.go @@ -4,9 +4,11 @@ import ( "fmt" "math/rand" "regexp" + "sort" "strings" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" kutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -140,24 +142,33 @@ func (g PipelineGroup) String() string { return strings.Join(s, "+") } -const MaxServiceNameLen = 24 +const maxServiceNameLength = 24 -var InvalidServiceChars = regexp.MustCompile("[^-a-z0-9]") +var invalidServiceChars = regexp.MustCompile("[^-a-z0-9]") -func makeValidServiceName(name string) string { - name = strings.ToLower(name) - name = InvalidServiceChars.ReplaceAllString(name, "") - if len(name) == 0 { - return fmt.Sprintf("svc-%d", rand.Intn(100000)) - } - if len(name) > MaxServiceNameLen-5 { - name = name[:MaxServiceNameLen-5] +func makeValidServiceName(name string) (string, string) { + if ok, _ := validation.ValidateServiceName(name, false); ok { + return name, "" } - name = fmt.Sprintf("%s-%d", name, rand.Intn(9999)) - if strings.HasPrefix(name, "-") { - name = "0" + name[1:] + name = strings.ToLower(name) + name = invalidServiceChars.ReplaceAllString(name, "") + name = strings.TrimFunc(name, func(r rune) bool { return r == '-' }) + switch { + case len(name) == 0: + return "", "service-" + case len(name) > maxServiceNameLength: + name = name[:maxServiceNameLength] + } + return name, "" +} + +func sortedPorts(ports []kapi.Port) []int { + result := []int{} + for _, p := range ports { + result = append(result, p.ContainerPort) } - return name + sort.Ints(result) + return result } func AddServices(objects Objects) Objects { @@ -166,11 +177,13 @@ func AddServices(objects Objects) Objects { switch t := o.(type) { case *deploy.DeploymentConfig: for _, container := range t.Template.ControllerTemplate.Template.Spec.Containers { - for _, port := range container.Ports { - p := port.ContainerPort + ports := sortedPorts(container.Ports) + for _, p := range ports { + name, generateName := makeValidServiceName(t.Name) svcs = append(svcs, &kapi.Service{ ObjectMeta: kapi.ObjectMeta{ - Name: makeValidServiceName(t.Name), + Name: name, + GenerateName: generateName, }, Spec: kapi.ServiceSpec{ ContainerPort: kutil.NewIntOrStringFromInt(p), @@ -178,6 +191,7 @@ func AddServices(objects Objects) Objects { Selector: t.Template.ControllerTemplate.Selector, }, }) + break } break }