/*
Copyright 2020 The Kubernetes Authors All rights reserved.

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.
*/

/*
The script releases current kic base image as stable, ie:
  - strips current version suffix starting from '-' in pkg/drivers/kic/types.go => release version
    (eg, 'v0.0.13-snapshot1' -> 'v0.0.13')
  - makes sure current kic base image exists locally, tries to pull one if not
  - tags current kic base image with the release version, and
  - pushes it to all relevant container registries

The script requires following credentials as env variables (injected by Jenkins credential provider):
  @GCR (ref: https://cloud.google.com/container-registry/docs/advanced-authentication):
  - GCR_USERNAME=<string>: GCR username, eg:
	= "oauth2accesstoken" if Access Token is used for GCR_TOKEN, or
	= "_json_key" if JSON Key File is used for GCR_TOKEN
  - GCR_TOKEN=<string>: GCR JSON token

  @Docker (ref: https://docs.docker.com/docker-hub/access-tokens/)
  - DOCKER_USERNAME=<string>: Docker username
  - DOCKER_TOKEN=<string>: Docker personal access token or password

  @GitHub (ref: https://docs.github.com/en/free-pro-team@latest/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages)
  - GITHUB_USERNAME=<string>: GitHub username
  - GITHUB_TOKEN=<string>: GitHub [personal] access token
*/

package main

import (
	"bytes"
	"context"
	"flag"
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"k8s.io/klog/v2"

	"github.com/pkg/errors"
)

const (
	// default context timeout
	cxTimeout = 600 * time.Second
)

var (
	kicFile      = "../../pkg/drivers/kic/types.go"
	kicVersionRE = `Version = "(.*)"`

	// keep list of registries in sync with those in kicFile
	registries = []registry{
		{
			name:     "Google Cloud Container Registry",
			image:    "gcr.io/k8s-minikube/kicbase",
			username: os.Getenv("GCR_USERNAME"),
			password: os.Getenv("GCR_TOKEN"),
		},
		{
			name:     "Docker Hub Container Registry",
			image:    "docker.io/kicbase/stable",
			username: os.Getenv("DOCKER_USERNAME"),
			password: os.Getenv("DOCKER_TOKEN"),
		},
		{
			name:     "GitHub Packages Registry",
			image:    "docker.pkg.github.com/kubernetes/minikube/kicbase",
			username: os.Getenv("GITHUB_USERNAME"),
			password: os.Getenv("GITHUB_TOKEN"),
		},
	}

	run = func(cmd *exec.Cmd, stdin io.Reader) error {
		cmd.Stdin = stdin
		var out bytes.Buffer
		cmd.Stderr = &out
		if err := cmd.Run(); err != nil {
			return errors.Errorf("%s: %s", err.Error(), out.String())
		}
		return nil
	}
)

// container registry name, image path, credentials, and updated flag
type registry struct {
	name     string
	image    string
	username string
	password string
	updated  bool
}

func (r *registry) setUpdated(updated bool) {
	r.updated = updated
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), cxTimeout)
	defer cancel()

	// write log statements to stderr instead of to files
	if err := flag.Set("logtostderr", "true"); err != nil {
		fmt.Printf("Error setting 'logtostderr' klog flag: %v", err)
	}
	flag.Parse()
	defer klog.Flush()

	// determine current kic base image version
	vCurrent, err := getKICVersion()
	if err != nil {
		klog.Fatalf("failed getting current kic base image version: %v", err)
	}
	if len(vCurrent) == 0 {
		klog.Fatalf("cannot determine current kic base image version")
	}
	klog.Infof("current kic base image version: %s", vCurrent)

	// determine release kic base image version
	vRelease := strings.Split(vCurrent, "-")[0]
	klog.Infof("release kic base image version: %s", vRelease)

	// prepare local kic base image
	image, err := prepareImage(ctx, vCurrent, vRelease)
	if err != nil {
		klog.Fatalf("failed preparing local kic base reference image: %v", err)
	}
	klog.Infof("local kic base reference image: %s", image)

	// update registries
	if updated := updateRegistries(ctx, image, vRelease); !updated {
		klog.Fatalf("failed updating all registries")
	}
}

// updateRegistries tags image with release version, pushes it to registries, and returns if any registry got updated
func updateRegistries(ctx context.Context, image, release string) (updated bool) {
	for _, reg := range registries {
		login := exec.CommandContext(ctx, "docker", "login", "--username", reg.username, "--password-stdin", reg.image)
		if err := run(login, strings.NewReader(reg.password)); err != nil {
			klog.Errorf("failed logging in to %s: %v", reg.name, err)
			continue
		}
		klog.Infof("successfully logged in to %s", reg.name)

		tag := exec.CommandContext(ctx, "docker", "tag", image+":"+release, reg.image+":"+release)
		if err := run(tag, nil); err != nil {
			klog.Errorf("failed tagging %s for %s: %v", reg.image+":"+release, reg.name, err)
			continue
		}
		klog.Infof("successfully tagged %s for %s", reg.image+":"+release, reg.name)

		push := exec.CommandContext(ctx, "docker", "push", reg.image+":"+release)
		if err := run(push, nil); err != nil {
			klog.Errorf("failed pushing %s to %s: %v", reg.image+":"+release, reg.name, err)
			continue
		}
		klog.Infof("successfully pushed %s to %s", reg.image+":"+release, reg.name)

		reg.setUpdated(true)
		klog.Infof("successfully updated %s", reg.name)
		updated = true
	}
	return updated
}

// prepareImage checks if current image exists locally, tries to pull it if not,
// tags it with release version, returns reference image url and any error
func prepareImage(ctx context.Context, current, release string) (image string, err error) {
	// check if image exists locally
	for _, reg := range registries {
		inspect := exec.CommandContext(ctx, "docker", "inspect", reg.image+":"+current, "--format", "{{.Id}}")
		if err := run(inspect, nil); err != nil {
			continue
		}
		image = reg.image
		break
	}
	if image == "" {
		// try to pull image locally
		for _, reg := range registries {
			pull := exec.CommandContext(ctx, "docker", "pull", reg.image+":"+current)
			if err := run(pull, nil); err != nil {
				continue
			}
			image = reg.image
			break
		}
	}
	if image == "" {
		return "", errors.Errorf("cannot find current image version tag %s locally nor in any registry", current)
	}
	// tag current image with release version
	tag := exec.CommandContext(ctx, "docker", "tag", image+":"+current, image+":"+release)
	if err := run(tag, nil); err != nil {
		return "", err
	}
	return image, nil
}

// getKICVersion returns current kic base image version and any error
func getKICVersion() (string, error) {
	blob, err := os.ReadFile(kicFile)
	if err != nil {
		return "", err
	}
	re := regexp.MustCompile(kicVersionRE)
	ver := re.FindSubmatch(blob)
	if ver == nil {
		return "", nil
	}
	return string(ver[1]), nil
}
