/*
Copyright 2017 The Kubernetes Authors.

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 selfhosting

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"time"

	"k8s.io/api/core/v1"
	extensions "k8s.io/api/extensions/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kuberuntime "k8s.io/apimachinery/pkg/runtime"
	clientset "k8s.io/client-go/kubernetes"
	kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
	kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
	kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
	"k8s.io/kubernetes/pkg/api"
)

const (
	kubeAPIServer         = "kube-apiserver"
	kubeControllerManager = "kube-controller-manager"
	kubeScheduler         = "kube-scheduler"

	selfHostingPrefix = "self-hosted-"
)

// CreateSelfHostedControlPlane is responsible for turning a Static Pod-hosted control plane to a self-hosted one
// It achieves that task this way:
// 1. Load the Static Pod specification from disk (from /etc/kubernetes/manifests)
// 2. Extract the PodSpec from that Static Pod specification
// 3. Mutate the PodSpec to be compatible with self-hosting (add the right labels, taints, etc. so it can schedule correctly)
// 4. Build a new DaemonSet object for the self-hosted component in question. Use the above mentioned PodSpec
// 5. Create the DaemonSet resource. Wait until the Pods are running.
// 6. Remove the Static Pod manifest file. The kubelet will stop the original Static Pod-hosted component that was running.
// 7. The self-hosted containers should now step up and take over.
// 8. In order to avoid race conditions, we're still making sure the API /healthz endpoint is healthy
// 9. Do that for the kube-apiserver, kube-controller-manager and kube-scheduler in a loop
func CreateSelfHostedControlPlane(cfg *kubeadmapi.MasterConfiguration, client clientset.Interface) error {

	if err := createTLSSecrets(cfg, client); err != nil {
		return err
	}

	if err := createOpaqueSecrets(cfg, client); err != nil {
		return err
	}

	// The sequence here isn't set in stone, but seems to work well to start with the API server
	components := []string{kubeAPIServer, kubeControllerManager, kubeScheduler}

	for _, componentName := range components {
		start := time.Now()
		manifestPath := buildStaticManifestFilepath(componentName)

		// Load the Static Pod file in order to be able to create a self-hosted variant of that file
		podSpec, err := loadPodSpecFromFile(manifestPath)
		if err != nil {
			return err
		}

		// Build a DaemonSet object from the loaded PodSpec
		ds := buildDaemonSet(cfg, componentName, podSpec)

		// Create the DaemonSet in the API Server
		if _, err := client.ExtensionsV1beta1().DaemonSets(metav1.NamespaceSystem).Create(ds); err != nil {
			if !apierrors.IsAlreadyExists(err) {
				return fmt.Errorf("failed to create self-hosted %q daemonset [%v]", componentName, err)
			}

			if _, err := client.ExtensionsV1beta1().DaemonSets(metav1.NamespaceSystem).Update(ds); err != nil {
				// TODO: We should retry on 409 responses
				return fmt.Errorf("failed to update self-hosted %q daemonset [%v]", componentName, err)
			}
		}

		// Wait for the self-hosted component to come up
		kubeadmutil.WaitForPodsWithLabel(client, buildSelfHostedWorkloadLabelQuery(componentName))

		// Remove the old Static Pod manifest
		if err := os.RemoveAll(manifestPath); err != nil {
			return fmt.Errorf("unable to delete static pod manifest for %s [%v]", componentName, err)
		}

		// Make sure the API is responsive at /healthz
		kubeadmutil.WaitForAPI(client)

		fmt.Printf("[self-hosted] self-hosted %s ready after %f seconds\n", componentName, time.Since(start).Seconds())
	}
	return nil
}

// buildDaemonSet is responsible for mutating the PodSpec and return a DaemonSet which is suitable for the self-hosting purporse
func buildDaemonSet(cfg *kubeadmapi.MasterConfiguration, name string, podSpec *v1.PodSpec) *extensions.DaemonSet {
	// Mutate the PodSpec so it's suitable for self-hosting
	mutatePodSpec(cfg, name, podSpec)

	// Return a DaemonSet based on that Spec
	return &extensions.DaemonSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      addSelfHostedPrefix(name),
			Namespace: metav1.NamespaceSystem,
			Labels: map[string]string{
				"k8s-app": addSelfHostedPrefix(name),
			},
		},
		Spec: extensions.DaemonSetSpec{
			Template: v1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"k8s-app": addSelfHostedPrefix(name),
					},
				},
				Spec: *podSpec,
			},
		},
	}
}

// loadPodSpecFromFile reads and decodes a file containing a specification of a Pod
// TODO: Consider using "k8s.io/kubernetes/pkg/volume/util".LoadPodFromFile(filename string) in the future instead.
func loadPodSpecFromFile(manifestPath string) (*v1.PodSpec, error) {
	podBytes, err := ioutil.ReadFile(manifestPath)
	if err != nil {
		return nil, err
	}

	staticPod := &v1.Pod{}
	if err := kuberuntime.DecodeInto(api.Codecs.UniversalDecoder(), podBytes, staticPod); err != nil {
		return nil, fmt.Errorf("unable to decode static pod %v", err)
	}

	return &staticPod.Spec, nil
}

// buildStaticManifestFilepath returns the location on the disk where the Static Pod should be present
func buildStaticManifestFilepath(componentName string) string {
	return filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName, componentName+".yaml")
}

// buildSelfHostedWorkloadLabelQuery creates the right query for matching a self-hosted Pod
func buildSelfHostedWorkloadLabelQuery(componentName string) string {
	return fmt.Sprintf("k8s-app=%s", addSelfHostedPrefix(componentName))
}

// addSelfHostedPrefix adds the self-hosted- prefix to the component name
func addSelfHostedPrefix(componentName string) string {
	return fmt.Sprintf("%s%s", selfHostingPrefix, componentName)
}
