func TestParseImage(t *testing.T) { for in, want := range imageParsingExamples { outReg, outName, outTag := flux.ParseImageID(in).Components() if outReg != want.Registry || outName != want.Name || outTag != want.Tag { t.Fatalf("%s: %v != %v", in, image{outReg, outName, outTag}, want) } } }
func containers2containers(cs []platform.Container) []flux.Container { res := make([]flux.Container, len(cs)) for i, c := range cs { res[i] = flux.Container{ Name: c.Name, Current: flux.ImageDescription{ ID: flux.ParseImageID(c.Image), }, } } return res }
func containersWithAvailable(service platform.Service, images instance.ImageMap) (res []flux.Container) { for _, c := range service.ContainersOrNil() { id := flux.ParseImageID(c.Image) repo := id.Repository() available := images[repo] res = append(res, flux.Container{ Name: c.Name, Current: flux.ImageDescription{ ID: id, }, Available: available, }) } return res }
func TestImageRepository(t *testing.T) { for in, want := range map[string]string{ "foo/bar": "foo/bar", "foo/bar:baz": "foo/bar", "reg:123/foo/bar:baz": "reg:123/foo/bar", "docker-registry.domain.name:5000/repo/image1:ver": "docker-registry.domain.name:5000/repo/image1", "shortreg/repo/image1": "shortreg/repo/image1", "foo": "foo", } { out := flux.ParseImageID(in).Repository() if out != want { t.Fatalf("%#v.Repository(): %s != %s", in, out, want) } } }
// Get the images available for the services given. An image may be // mentioned more than once in the services, but will only be fetched // once. func (h *Instance) CollectAvailableImages(services []platform.Service) (ImageMap, error) { images := ImageMap{} for _, service := range services { for _, container := range service.ContainersOrNil() { repo := flux.ParseImageID(container.Image).Repository() images[repo] = nil } } for repo := range images { imageRepo, err := h.registry.GetRepository(repo) if err != nil { return nil, errors.Wrapf(err, "fetching image metadata for %s", repo) } images[repo] = imageRepo } return images, nil }
// Attempt to update an RC or Deployment config. This makes several assumptions // that are justified only with the phrase "because that's how we do it", // including: // // * the file is a replication controller or deployment // * the update is from one tag of an image to another tag of the // same image; e.g., "weaveworks/helloworld:a00001" to // "weaveworks/helloworld:a00002" // * the container spec to update is the (first) one that uses the // same image name (e.g., weaveworks/helloworld) // * the name of the controller is updated to reflect the new tag // * there's a label which must be updated in both the pod spec and the selector // * the file uses canonical YAML syntax, that is, one line per item // * ... other assumptions as encoded in the regular expressions used // // Here's an example of the assumed structure: // // ``` // apiVersion: v1 // kind: ReplicationController # not presently checked // metadata: # ) // ... # ) any number of equally-indented lines // name: helloworld-master-a000001 # ) can precede the name // spec: // replicas: 2 // selector: # ) // name: helloworld # ) this use of labels is assumed // version: master-a000001 # ) // template: // metadata: // labels: # ) // name: helloworld # ) this structure is assumed, as for the selector // version: master-a000001 # ) // spec: // containers: // # extra container specs are allowed here ... // - name: helloworld # ) // image: quay.io/weaveworks/helloworld:master-a000001 # ) these must be together // args: // - -msg=Ahoy // ports: // - containerPort: 80 // ``` func tryUpdate(def, newImageStr string, trace io.Writer, out io.Writer) error { newImage := flux.ParseImageID(newImageStr) nameRE := multilineRE( `metadata:\s*`, `(?: .*\n)* name:\s*"?([\w-]+)"?\s*`, ) matches := nameRE.FindStringSubmatch(def) if matches == nil || len(matches) < 2 { return fmt.Errorf("Could not find resource name") } oldDefName := matches[1] fmt.Fprintf(trace, "Found resource name %q in fragment:\n\n%s\n\n", oldDefName, matches[0]) imageRE := multilineRE( ` containers:.*`, `(?: .*\n)*(?: ){3,4}- name:\s*"?([\w-]+)"?(?:\s.*)?`, `(?: ){4,5}image:\s*"?(`+newImage.Repository()+`:[\w][\w.-]{0,127})"?(\s.*)?`, ) // tag part of regexp from // https://github.com/docker/distribution/blob/master/reference/regexp.go#L36 matches = imageRE.FindStringSubmatch(def) if matches == nil || len(matches) < 3 { return fmt.Errorf("Could not find image name") } containerName := matches[1] oldImage := flux.ParseImageID(matches[2]) fmt.Fprintf(trace, "Found container %q using image %v in fragment:\n\n%s\n\n", containerName, oldImage, matches[0]) if oldImage.Repository() != newImage.Repository() { return fmt.Errorf(`expected existing image name and new image name to match, but %q != %q`, oldImage.Repository(), newImage.Repository()) } // Now to replace bits. Specifically, // * the name, with a re-tagged name // * the image for the container // * the version label (in two places) // // Some values (most likely the version) will be interpreted as a // number if unquoted; while, on the other hand, it is apparently // not OK to quote things that don't look like numbers. So: we // extract values *without* quotes, and add them if necessary. newDefName := oldDefName _, _, oldImageTag := oldImage.Components() _, _, newImageTag := newImage.Components() if strings.HasSuffix(oldDefName, oldImageTag) { newDefName = oldDefName[:len(oldDefName)-len(oldImageTag)] + newImageTag } newDefName = maybeQuote(newDefName) newTag := maybeQuote(newImageTag) fmt.Fprintln(trace, "") fmt.Fprintln(trace, "Replacing ...") fmt.Fprintf(trace, "Resource name: %s -> %s\n", oldDefName, newDefName) fmt.Fprintf(trace, "Version in templates (and selector if present): %s -> %s\n", oldImageTag, newTag) fmt.Fprintf(trace, "Image in templates: %s -> %s\n", oldImage, newImage) fmt.Fprintln(trace, "") // The name we want is that under `metadata:`, which will be indented once replaceRCNameRE := regexp.MustCompile(`(?m:^( name:\s*) (?:"?[\w-]+"?)(\s.*)$)`) withNewDefName := replaceRCNameRE.ReplaceAllString(def, fmt.Sprintf(`$1 %s$2`, newDefName)) // Replacing labels: these are in two places, the container template and the selector replaceLabelsRE := multilineRE( `((?: selector| labels):.*)`, `((?: ){2,4}name:.*)`, `((?: ){2,4}version:\s*) (?:"?[-\w]+"?)(\s.*)`, ) replaceLabels := fmt.Sprintf("$1\n$2\n$3 %s$4", newTag) withNewLabels := replaceLabelsRE.ReplaceAllString(withNewDefName, replaceLabels) replaceImageRE := multilineRE( `((?: ){3,4}- name:\s*`+containerName+`)`, `((?: ){4,5}image:\s*) .*`, ) replaceImage := fmt.Sprintf("$1\n$2 %s$3", string(newImage)) withNewImage := replaceImageRE.ReplaceAllString(withNewLabels, replaceImage) fmt.Fprint(out, withNewImage) return nil }
func (r *Releaser) releaseImages(method, msg string, inst *instance.Instance, kind flux.ReleaseKind, getServices serviceQuery, getImages imageCollect, updateJob func(string, ...interface{})) (err error) { var res []ReleaseAction defer func() { if err == nil { err = r.execute(inst, res, kind, updateJob) } }() res = append(res, r.releaseActionPrintf(msg)) var ( base = r.metrics.StageDuration.With("method", method) stage *metrics.Timer ) defer func() { stage.ObserveDuration() }() stage = metrics.NewTimer(base.With("stage", "fetch_platform_services")) services, err := getServices(inst) if err != nil { return errors.Wrap(err, "fetching platform services") } stage.ObserveDuration() stage = metrics.NewTimer(base.With("stage", "calculate_regrades")) // Each service is running multiple images. // Each image may need to be upgraded, and trigger a release. images, err := getImages(inst, services) if err != nil { return errors.Wrap(err, "collecting available images to calculate regrades") } regradeMap := map[flux.ServiceID][]containerRegrade{} for _, service := range services { containers, err := service.ContainersOrError() if err != nil { res = append(res, r.releaseActionPrintf("service %s does not have images associated: %s", service.ID, err)) continue } for _, container := range containers { currentImageID := flux.ParseImageID(container.Image) latestImage := images.LatestImage(currentImageID.Repository()) if latestImage == nil { continue } if currentImageID == latestImage.ID { res = append(res, r.releaseActionPrintf("Service %s image %s is already the latest one; skipping.", service.ID, currentImageID)) continue } regradeMap[service.ID] = append(regradeMap[service.ID], containerRegrade{ container: container.Name, current: currentImageID, target: latestImage.ID, }) } } if len(regradeMap) <= 0 { res = append(res, r.releaseActionPrintf("All selected services are running the requested images. Nothing to do.")) return nil } stage.ObserveDuration() stage = metrics.NewTimer(base.With("stage", "finalize")) // We have identified at least 1 release that needs to occur. Releasing // means cloning the repo, changing the resource file(s), committing and // pushing, and then making the release(s) to the platform. res = append(res, r.releaseActionClone()) for service, regrades := range regradeMap { res = append(res, r.releaseActionUpdatePodController(service, regrades)) } res = append(res, r.releaseActionCommitAndPush(msg)) var servicesToRegrade []flux.ServiceID for service := range regradeMap { servicesToRegrade = append(servicesToRegrade, service) } res = append(res, r.releaseActionRegradeServices(servicesToRegrade, msg)) return nil }
func (r *Releaser) Handle(job *jobs.Job, updater jobs.JobUpdater) (err error) { spec := job.Params.(jobs.ReleaseJobParams) releaseType := "unknown" defer func(begin time.Time) { r.metrics.ReleaseDuration.With( "release_type", releaseType, "release_kind", fmt.Sprint(spec.Kind), "success", fmt.Sprint(err == nil), ).Observe(time.Since(begin).Seconds()) }(time.Now()) inst, err := r.instancer.Get(job.Instance) if err != nil { return err } inst.Logger = log.NewContext(inst.Logger).With("job", job.ID) updateJob := func(format string, args ...interface{}) { status := fmt.Sprintf(format, args...) job.Status = status job.Log = append(job.Log, status) updater.UpdateJob(*job) } exclude := flux.ServiceIDSet{} exclude.Add(spec.Excludes) locked, err := lockedServices(inst) if err != nil { return err } exclude.Add(locked) updateJob("Calculating release actions.") switch { case spec.ServiceSpec == flux.ServiceSpecAll && spec.ImageSpec == flux.ImageSpecLatest: releaseType = "release_all_to_latest" return r.releaseImages(releaseType, "Release latest images to all services", inst, spec.Kind, allServicesExcept(exclude), allLatestImages, updateJob) case spec.ServiceSpec == flux.ServiceSpecAll && spec.ImageSpec == flux.ImageSpecNone: releaseType = "release_all_without_update" return r.releaseWithoutUpdate(releaseType, "Apply latest config to all services", inst, spec.Kind, allServicesExcept(exclude), updateJob) case spec.ServiceSpec == flux.ServiceSpecAll: releaseType = "release_all_for_image" imageID := flux.ParseImageID(string(spec.ImageSpec)) return r.releaseImages(releaseType, fmt.Sprintf("Release %s to all services", imageID), inst, spec.Kind, allServicesExcept(exclude), exactlyTheseImages([]flux.ImageID{imageID}), updateJob) case spec.ImageSpec == flux.ImageSpecLatest: releaseType = "release_one_to_latest" serviceID, err := flux.ParseServiceID(string(spec.ServiceSpec)) if err != nil { return errors.Wrapf(err, "parsing service ID from spec %s", spec.ServiceSpec) } services := flux.ServiceIDs([]flux.ServiceID{serviceID}).Without(exclude) return r.releaseImages(releaseType, fmt.Sprintf("Release latest images to %s", serviceID), inst, spec.Kind, exactlyTheseServices(services), allLatestImages, updateJob) case spec.ImageSpec == flux.ImageSpecNone: releaseType = "release_one_without_update" serviceID, err := flux.ParseServiceID(string(spec.ServiceSpec)) if err != nil { return errors.Wrapf(err, "parsing service ID from spec %s", spec.ServiceSpec) } services := flux.ServiceIDs([]flux.ServiceID{serviceID}).Without(exclude) return r.releaseWithoutUpdate(releaseType, fmt.Sprintf("Apply latest config to %s", serviceID), inst, spec.Kind, exactlyTheseServices(services), updateJob) default: releaseType = "release_one" serviceID, err := flux.ParseServiceID(string(spec.ServiceSpec)) if err != nil { return errors.Wrapf(err, "parsing service ID from spec %s", spec.ServiceSpec) } services := flux.ServiceIDs([]flux.ServiceID{serviceID}).Without(exclude) imageID := flux.ParseImageID(string(spec.ImageSpec)) return r.releaseImages(releaseType, fmt.Sprintf("Release %s to %s", imageID, serviceID), inst, spec.Kind, exactlyTheseServices(services), exactlyTheseImages([]flux.ImageID{imageID}), updateJob) } }