Esempio n. 1
0
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)
		}
	}
}
Esempio n. 2
0
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
}
Esempio n. 3
0
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
}
Esempio n. 4
0
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)
		}
	}
}
Esempio n. 5
0
// 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
}
Esempio n. 6
0
// 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
}
Esempio n. 7
0
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
}
Esempio n. 8
0
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)
	}
}