pkg/docker.go (255 lines of code) (raw):

// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package docker import ( "archive/tar" "bytes" "context" "encoding/json" "fmt" "io" "regexp" "sort" "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/spf13/cobra" ) func EditTagSuffixWrapper(cmd *cobra.Command, args []string, appendOrRemove bool) error { tagSuffix := args[1] if tagSuffix == "" { return fmt.Errorf("TAG_SUFFIX cannot be empty") } r, err := MakeRegex(args[0]) if err != nil { return err } dcli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return err } return editTagSuffix(dcli, tagSuffix, appendOrRemove, r) } func GetImageAndTag(repoTag string) (string, string, error) { imageAndTag := strings.Split(repoTag, ":") if len(imageAndTag) != 2 { return "", "", fmt.Errorf("divisor ':' not found in RepoTag %v", repoTag) } return imageAndTag[0], imageAndTag[1], nil } // "A tag name must be valid ASCII and may contain lowercase and uppercase // letters, digits, underscores, periods and dashes. A tag name may not start // with a period or a dash and may contain a maximum of 128 characters." [1] // // [1]: https://docs.docker.com/engine/reference/commandline/tag/ func isValidTag(tag string) bool { r, _ := MakeRegex("^[a-zA-Z0-9_][a-zA-Z0-9_.-]+$") if !r.MatchString(tag) { return false } if len(tag) > 128 { return false } return true } func repoTagExists(dcli *client.Client, needle string) bool { images, err := dcli.ImageList(context.Background(), types.ImageListOptions{All: true}) if err != nil { return false } for _, image := range images { for _, repoTag := range image.RepoTags { if repoTag == needle { return true } } } return false } type TagOp struct { From string To string } func appendTag(tagOps []TagOp, dcli *client.Client, tagSuffix string, repoTag string) ([]TagOp, error) { imageName, tag, err := GetImageAndTag(repoTag) if err != nil { return tagOps, err } // Skip implicit "latest" tag. Images should not be named // "latest-<suffix>" (or seen another way, have a "latest-" tag // prefix). if tag == "latest" { fmt.Printf("skipping %v (avoid tagging '%v-%v')\n", repoTag, tag, tagSuffix) return tagOps, nil } if strings.HasSuffix(repoTag, "-"+tagSuffix) { fmt.Printf("skipping %v (already has suffix '-%v')\n", repoTag, tagSuffix) return tagOps, nil } var newTag string = tag + "-" + tagSuffix if !isValidTag(newTag) { return tagOps, fmt.Errorf("new tag %v is invalid", newTag) } var newRepoTag string = imageName + ":" + newTag if repoTagExists(dcli, newRepoTag) { fmt.Printf("skipping %v (already suffixed to '-%v')\n", repoTag, tagSuffix) return tagOps, nil } tagOps = append(tagOps, TagOp{repoTag, newRepoTag}) return tagOps, nil } func removeTag(tagOps []TagOp, dcli *client.Client, tagSuffix string, repoTag string) ([]TagOp, error) { imageName, tag, err := GetImageAndTag(repoTag) if err != nil { return tagOps, err } var newTag string = strings.TrimSuffix(tag, "-"+tagSuffix) var newRepoTag string = imageName + ":" + newTag if newRepoTag == repoTag { fmt.Printf("skipping %v (suffix '-%v' not found)\n", repoTag, tagSuffix) return tagOps, nil } tagOps = append(tagOps, TagOp{repoTag, newRepoTag}) return tagOps, nil } func mkTaggingOperations(dcli *client.Client, tagSuffix string, r *regexp.Regexp, appendOrRemove bool) ([]TagOp, error) { images, err := FindImages(dcli, r) if err != nil { return nil, err } tagOps := make([]TagOp, 0) for _, image := range images { // Skip untagged (dangling) images. if image.RepoTags[0] == "<none>:<none>" { continue } for _, repoTag := range image.RepoTags { if !r.MatchString(repoTag) { continue } if appendOrRemove { tagOps, err = appendTag(tagOps, dcli, tagSuffix, repoTag) } else { tagOps, err = removeTag(tagOps, dcli, tagSuffix, repoTag) } if err != nil { return nil, err } } } return tagOps, nil } func editTagSuffix(dcli *client.Client, tagSuffix string, appendOrRemove bool, r *regexp.Regexp) error { ops, err := mkTaggingOperations(dcli, tagSuffix, r, appendOrRemove) if err != nil { return err } if len(ops) == 0 { fmt.Printf("Nothing to do.\n") return nil } for _, op := range ops { return MoveTag(dcli, op) } return nil } func MoveTag(dcli *client.Client, tagOp TagOp) error { err := dcli.ImageTag(context.Background(), tagOp.From, tagOp.To) if err != nil { return err } fmt.Printf("tagged from:%v\n to:%v\n", tagOp.From, tagOp.To) responses, err := dcli.ImageRemove(context.Background(), tagOp.From, types.ImageRemoveOptions{}) for _, res := range responses { if len(res.Deleted) > 0 { fmt.Printf("deleted: %v\n", res.Deleted) } if len(res.Untagged) > 0 { fmt.Printf("untagged: %v\n", res.Untagged) } } return nil } type ImageMap map[string]types.ImageSummary func FindImages(dcli *client.Client, r *regexp.Regexp) (ImageMap, error) { found := make(ImageMap) images, err := dcli.ImageList(context.Background(), types.ImageListOptions{All: true}) if err != nil { return nil, err } for _, image := range images { if len(image.RepoTags) == 0 || image.RepoTags[0] == "<none>:<none>" { continue } for _, repoTag := range image.RepoTags { if r.MatchString(repoTag) { found[repoTag] = image } } } return found, nil } func BuildImage(dcli *client.Client, dockerFileContents []byte, labels map[string]string, tags []string) error { buf := new(bytes.Buffer) tw := tar.NewWriter(buf) defer tw.Close() tarHeader := &tar.Header{ Name: "Dockerfile", Size: int64(len(dockerFileContents)), } err := tw.WriteHeader(tarHeader) if err != nil { return err } _, err = tw.Write(dockerFileContents) if err != nil { return err } dockerFileTarReader := bytes.NewReader(buf.Bytes()) ctx := context.Background() imageBuildResponse, err := dcli.ImageBuild( ctx, dockerFileTarReader, types.ImageBuildOptions{ Context: dockerFileTarReader, Labels: labels, Tags: tags, // Remove: whether to remove the intermediate container used during // the build. Remove: true}) if err != nil { return err } err = PrintStream(ctx, imageBuildResponse.Body) if err != nil { return err } return nil } type TextStream struct { Stream string `json:"stream"` } func PrintStream(ctx context.Context, stream io.ReadCloser) error { decoder := json.NewDecoder(stream) var s TextStream for { select { case <-ctx.Done(): stream.Close() return nil default: if err := decoder.Decode(&s); err == io.EOF { return nil } else if err != nil { return err } } fmt.Print(s.Stream) } } func MakeRegex(regex string) (*regexp.Regexp, error) { if regex == "" { return nil, fmt.Errorf("REGEX cannot be empty") } return regexp.Compile(regex) } func (images ImageMap) SortedNames() []string { imageNames := make([]string, 0) for imageName, _ := range images { imageNames = append(imageNames, imageName) } sort.Strings(imageNames) return imageNames } func (images ImageMap) ShowPretty() { for _, imageName := range images.SortedNames() { fmt.Printf(" - %v\n", imageName) } }