blob: 01fa8c2393bd8910a8d1c63c8b67aa33303afc27 [file] [log] [blame]
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"go.skia.org/infra/cd/go/stages"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/docker"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/k8s"
"go.skia.org/infra/go/kube/clusterconfig"
"go.skia.org/infra/go/repo_root"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"golang.org/x/oauth2/google"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func main() {
ctx := context.Background()
// Parse and validate flags.
var (
commit = flag.String("commit", "", "Commit hash or reference to test")
repoURL = flag.String("repo", common.REPO_SKIA_INFRA, "Git repo URL to test")
// --image and --container are mutually exclusive.
image = flag.String("image", "", "Fully-qualified Docker image ID")
container = flag.String("container", "", "String or regular expression indicating which live containers to check")
// These are only valid with --container.
cluster = flag.String("cluster", "", "Optional. Kubernetes cluster of the container to test")
namespace = flag.String("namespace", "", "Optional. Kubernetes namespace of the container to test")
kubeConfig = flag.String("kube_config", "", "Path to Kubernetes config file. If not provided, attempt to find in HOME")
clusterConfigFile = flag.String("cluster_config", "", "Path to cluster config file. If not provided, attempt to find in the current repo.")
)
flag.Parse()
if *commit == "" {
sklog.Fatal("--commit is required")
}
if (*image == "" && *container == "") || (*image != "" && *container != "") {
sklog.Fatal("Exactly one of --image or --container is required")
}
if *container == "" {
if *cluster != "" {
sklog.Fatal("--cluster is only valid with --container")
}
if *namespace != "" {
sklog.Fatal("--namespace is only valid with --container")
}
}
// Set up clients.
ts, err := google.DefaultTokenSource(ctx, gerrit.AuthScope)
if err != nil {
sklog.Fatal(err)
}
httpClient := httputils.DefaultClientConfig().With2xxAnd3xx().WithTokenSource(ts).Client()
repo := gitiles.NewRepo(*repoURL, httpClient)
dockerClient, err := docker.NewClient(ctx)
if err != nil {
sklog.Fatal(err)
}
// Find (if necessary) and check the image(s).
if *image != "" {
isIncluded, err := isCommitInImage(ctx, dockerClient, repo, *commit, *image)
if err != nil {
sklog.Fatal(err)
}
fmt.Printf("%s: %v\n", *image, isIncluded)
} else {
containerRegex, err := regexp.Compile(*container)
if err != nil {
sklog.Fatal(err)
}
clusterRegex, err := regexp.Compile(*cluster)
if err != nil {
sklog.Fatal(err)
}
namespaceRegex, err := regexp.Compile(*namespace)
if err != nil {
sklog.Fatal(err)
}
if *kubeConfig == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
sklog.Fatalf("Failed to find home directory. Maybe supply --kube_config? %s", err)
}
*kubeConfig = filepath.Join(homeDir, ".kube", "config")
}
if _, err := os.Stat(*kubeConfig); os.IsNotExist(err) {
sklog.Fatalf("Failed to find kubeconfig at %s; try a different value for --kube_config? %s", *kubeConfig, err)
}
if *clusterConfigFile == "" {
repoRoot, err := repo_root.GetLocal()
if err != nil {
sklog.Fatalf("Failed to find repo root. Maybe supply --cluster_config")
}
*clusterConfigFile = filepath.Join(repoRoot, "kube", "clusters", "config.json")
}
if _, err := os.Stat(*clusterConfigFile); os.IsNotExist(err) {
sklog.Fatalf("Failed to find cluster config at %s; try a different value for --cluster_config? %s", *clusterConfigFile, err)
}
imageToContainers, err := findMatchingContainerImages(ctx, *kubeConfig, *clusterConfigFile, clusterRegex, namespaceRegex, containerRegex)
if err != nil {
sklog.Fatal(err)
}
if len(imageToContainers) == 0 {
sklog.Fatal("Found no images matching `%s`", *container)
}
for image := range imageToContainers {
isIncluded, err := isCommitInImage(ctx, dockerClient, repo, *commit, image)
if err != nil {
sklog.Fatal(err)
}
fmt.Printf("%s: %v\n", image, isIncluded)
for _, container := range imageToContainers[image] {
fmt.Printf(" %s\n", container)
}
}
}
}
// findMatchingContainerImages returns a mapping of image ID to slices of
// container names which are running each image.
func findMatchingContainerImages(ctx context.Context, kubeConfigFile, clusterConfigFile string, clusterRegex, namespaceRegex, containerRegex *regexp.Regexp) (map[string][]string, error) {
clusterConfig, err := clusterconfig.New(clusterConfigFile)
if err != nil {
return nil, skerr.Wrap(err)
}
imageToContainers := map[string][]string{}
for cluster := range clusterConfig.Clusters {
if cluster == "skia-corp" || cluster == "skia-switchboard" {
// TODO(borenet): These clusters no longer exist; remove from config?
continue
}
if !clusterRegex.MatchString(cluster) {
continue
}
client, err := k8s.NewLocalClient(ctx, kubeConfigFile, clusterConfigFile, cluster)
if err != nil {
return nil, skerr.Wrap(err)
}
namespaces, err := client.ListNamespaces(ctx, v1.ListOptions{})
if err != nil {
return nil, skerr.Wrap(err)
}
for _, ns := range namespaces {
if !namespaceRegex.MatchString(ns.Name) {
continue
}
pods, err := client.ListPods(ctx, ns.Name, v1.ListOptions{})
if err != nil {
return nil, skerr.Wrap(err)
}
for _, pod := range pods {
for _, container := range pod.Spec.Containers {
if containerRegex.MatchString(container.Name) {
imageToContainers[container.Image] = append(imageToContainers[container.Image], container.Name)
}
}
}
}
}
return imageToContainers, nil
}
// isCommitInImage finds the commit hashes associated with the given image via
// tags, then checks to see whether the given commit is an ancestor of any of
// those commits.
func isCommitInImage(ctx context.Context, dockerClient docker.Client, repo *gitiles.Repo, commit, image string) (bool, error) {
registry, repository, tagOrDigest, err := docker.SplitImage(image)
if err != nil {
return false, skerr.Wrap(err)
}
digest, err := docker.GetDigestIfNeeded(ctx, dockerClient, registry, repository, tagOrDigest)
if err != nil {
return false, skerr.Wrap(err)
}
commitHashes, err := stages.GetCommitHashesForImage(ctx, dockerClient, registry, repository, digest)
if err != nil {
return false, skerr.Wrap(err)
}
for _, hash := range commitHashes {
if hash == commit {
return true, nil
}
}
for _, hash := range commitHashes {
commits, err := repo.Log(ctx, fmt.Sprintf("%s..%s", commit, hash))
if err != nil {
return false, skerr.Wrap(err)
}
if len(commits) > 0 {
return true, nil
}
}
return false, nil
}