Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
environment.go 10.30 KiB
// Copyright 2017 The kubecfg 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 metadata

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	log "github.com/sirupsen/logrus"
	"github.com/spf13/afero"

	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
)

const (
	defaultEnvName  = "default"
	metadataDirName = ".metadata"

	schemaFilename        = "swagger.json"
	extensionsLibFilename = "k.libsonnet"
	k8sLibFilename        = "k8s.libsonnet"
	specFilename          = "spec.json"
)

// Environment represents all fields of a ksonnet environment
type Environment struct {
	Path string
	Name string
	URI  string
}

// EnvironmentSpec represents the contents in spec.json.
type EnvironmentSpec struct {
	URI string `json:"uri"`
}

func (m *manager) CreateEnvironment(name, uri string, spec ClusterSpec) error {
	extensionsLibData, k8sLibData, specData, err := m.generateKsonnetLibData(spec)
	if err != nil {
		log.Debugf("Failed to write '%s'", specFilename)
		return err
	}

	err = m.createEnvironment(name, uri, extensionsLibData, k8sLibData, specData)
	if err == nil {
		log.Infof("Environment '%s' pointing to cluster at URI '%s' successfully created", name, uri)
	}
	return err
}

func (m *manager) createEnvironment(name, uri string, extensionsLibData, k8sLibData, specData []byte) error {
	exists, err := m.environmentExists(name)
	if err != nil {
		log.Debug("Failed to check whether environment exists")
		return err
	}
	if exists {
		return fmt.Errorf("Environment '%s' already exists", name)
	}

	// ensure environment name does not contain punctuation
	if !isValidName(name) {
		return fmt.Errorf("Environment name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
	}

	log.Infof("Creating environment '%s' with uri '%s'", name, uri)

	envPath := appendToAbsPath(m.environmentsPath, name)
	err = m.appFS.MkdirAll(string(envPath), defaultFolderPermissions)
	if err != nil {
		return err
	}

	metadataPath := appendToAbsPath(envPath, metadataDirName)
	err = m.appFS.MkdirAll(string(metadataPath), defaultFolderPermissions)
	if err != nil {
		return err
	}

	log.Infof("Generating environment metadata at path '%s'", envPath)

	// Generate the schema file.
	log.Debugf("Generating '%s', length: %d", schemaFilename, len(specData))
	schemaPath := appendToAbsPath(metadataPath, schemaFilename)
	err = afero.WriteFile(m.appFS, string(schemaPath), specData, defaultFilePermissions)
	if err != nil {
		log.Debugf("Failed to write '%s'", schemaFilename)
		return err
	}

	log.Debugf("Generating '%s', length: %d", k8sLibFilename, len(k8sLibData))
	k8sLibPath := appendToAbsPath(metadataPath, k8sLibFilename)
	err = afero.WriteFile(m.appFS, string(k8sLibPath), k8sLibData, defaultFilePermissions)
	if err != nil {
		log.Debugf("Failed to write '%s'", k8sLibFilename)
		return err
	}

	log.Debugf("Generating '%s', length: %d", extensionsLibFilename, len(extensionsLibData))
	extensionsLibPath := appendToAbsPath(metadataPath, extensionsLibFilename)
	err = afero.WriteFile(m.appFS, string(extensionsLibPath), extensionsLibData, defaultFilePermissions)
	if err != nil {
		log.Debugf("Failed to write '%s'", extensionsLibFilename)
		return err
	}

	// Generate the environment spec file.
	envSpecData, err := generateSpecData(uri)
	if err != nil {
		return err
	}

	log.Debugf("Generating '%s', length: %d", specFilename, len(envSpecData))
	envSpecPath := appendToAbsPath(envPath, specFilename)
	return afero.WriteFile(m.appFS, string(envSpecPath), envSpecData, defaultFilePermissions)
}

func (m *manager) DeleteEnvironment(name string) error {
	envPath := string(appendToAbsPath(m.environmentsPath, name))

	// Check whether this environment exists
	envExists, err := m.environmentExists(name)
	if err != nil {
		log.Debug("Failed to check whether environment exists")
		return err
	}
	if !envExists {
		return fmt.Errorf("Environment '%s' does not exist", name)
	}

	log.Infof("Deleting environment '%s' at path '%s'", name, envPath)

	// Remove the directory and all files within the environment path.
	err = m.appFS.RemoveAll(envPath)
	if err != nil {
		log.Debugf("Failed to remove environment directory at path '%s'", envPath)
		return err
	}

	// Need to ensure empty parent directories are also removed.
	log.Debug("Removing empty parent directories, if any")
	parentDir := name
	for parentDir != "." {
		parentDir = filepath.Dir(parentDir)
		parentPath := string(appendToAbsPath(m.environmentsPath, parentDir))

		isEmpty, err := afero.IsEmpty(m.appFS, parentPath)
		if err != nil {
			log.Debugf("Failed to check whether parent directory at path '%s' is empty", parentPath)
			return err
		}
		if isEmpty {
			log.Debugf("Failed to remove parent directory at path '%s'", parentPath)
			err := m.appFS.RemoveAll(parentPath)
			if err != nil {
				return err
			}
		}
	}

	log.Infof("Successfully removed environment '%s'", name)
	return nil
}

func (m *manager) GetEnvironments() ([]*Environment, error) {
	envs := []*Environment{}

	log.Info("Retrieving all environments")
	err := afero.Walk(m.appFS, string(m.environmentsPath), func(path string, f os.FileInfo, err error) error {
		isDir, err := afero.IsDir(m.appFS, path)
		if err != nil {
			log.Debugf("Failed to check whether the path at '%s' is a directory", path)
			return err
		}

		if isDir {
			// Only want leaf directories containing a spec.json
			specPath := filepath.Join(path, specFilename)
			specFileExists, err := afero.Exists(m.appFS, specPath)
			if err != nil {
				log.Debugf("Failed to check whether spec file at '$s' exists", specPath)
				return err
			}
			if specFileExists {
				envName := filepath.Clean(strings.TrimPrefix(path, string(m.environmentsPath)+"/"))
				specFile, err := afero.ReadFile(m.appFS, specPath)
				if err != nil {
					log.Debugf("Failed to read spec file at path '%s'", specPath)
					return err
				}
				var envSpec EnvironmentSpec
				err = json.Unmarshal(specFile, &envSpec)
				if err != nil {
					log.Debugf("Failed to convert the spec file at path '%s' to JSON", specPath)
					return err
				}

				log.Debugf("Found environment '%s', with uri '%s", envName, envSpec.URI)
				envs = append(envs, &Environment{Name: envName, Path: path, URI: envSpec.URI})
			}
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return envs, nil
}

func (m *manager) GetEnvironment(name string) (*Environment, error) {
	envs, err := m.GetEnvironments()
	if err != nil {
		return nil, err
	}

	for _, env := range envs {
		if env.Name == name {
			return env, nil
		}
	}

	return nil, fmt.Errorf("Environment '%s' does not exist", name)
}

func (m *manager) SetEnvironment(name string, desired *Environment) error {
	// Check whether this environment exists
	envExists, err := m.environmentExists(name)
	if err != nil {
		log.Debugf("Failed to check whether '%s' exists", name)
		return err
	}
	if !envExists {
		return fmt.Errorf("Environment '%s' does not exist", name)
	}

	// If the name has changed, the directory location needs to be moved to
	// reflect the change.
	if name != desired.Name && len(desired.Name) != 0 {
		// ensure new environment name does not contain punctuation
		if !isValidName(desired.Name) {
			return fmt.Errorf("Environment name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
		}

		log.Infof("Setting environment name from '%s' to '%s'", name, desired.Name)

		// Ensure not overwriting another environment
		desiredExists, err := m.environmentExists(desired.Name)
		if err != nil {
			log.Debugf("Failed to check whether environment '%s' already exists", desired.Name)
			return err
		}
		if desiredExists {
			return fmt.Errorf("Can not update '%s' to '%s', it already exists", name, desired.Name)
		}

		// Move the directory
		pathOld := string(appendToAbsPath(m.environmentsPath, name))
		pathNew := string(appendToAbsPath(m.environmentsPath, desired.Name))
		log.Debugf("Moving directory at path '%s' to '%s'", pathOld, pathNew)
		err = m.appFS.Rename(pathOld, pathNew)
		if err != nil {
			log.Debugf("Failed to move path '%s' to '%s", pathOld, pathNew)
			return err
		}

		name = desired.Name
	}

	// Update fields in spec.json
	if len(desired.URI) != 0 {
		log.Infof("Setting environment URI to '%s'", desired.URI)

		newSpec, err := generateSpecData(desired.URI)
		if err != nil {
			log.Debugf("Failed to generate %s with URI '%s'", specFilename, desired.URI)
			return err
		}

		envPath := appendToAbsPath(m.environmentsPath, name)
		specPath := appendToAbsPath(envPath, specFilename)

		err = afero.WriteFile(m.appFS, string(specPath), newSpec, defaultFilePermissions)
		if err != nil {
			log.Debugf("Failed to write %s at path '%s'", specFilename, specPath)
			return err
		}
	}

	log.Infof("Successfully updated environment '%s'", name)
	return nil
}

func (m *manager) generateKsonnetLibData(spec ClusterSpec) ([]byte, []byte, []byte, error) {
	// Get cluster specification data, possibly from the network.
	text, err := spec.data()
	if err != nil {
		return nil, nil, nil, err
	}

	ksonnetLibDir := appendToAbsPath(m.environmentsPath, defaultEnvName)

	// Deserialize the API object.
	s := kubespec.APISpec{}
	err = json.Unmarshal(text, &s)
	if err != nil {
		return nil, nil, nil, err
	}

	s.Text = text
	s.FilePath = filepath.Dir(string(ksonnetLibDir))

	// Emit Jsonnet code.
	extensionsLibData, k8sLibData, err := ksonnet.Emit(&s, nil, nil)
	return extensionsLibData, k8sLibData, text, err
}

func generateSpecData(uri string) ([]byte, error) {
	// Format the spec json and return; preface keys with 2 space idents.
	return json.MarshalIndent(EnvironmentSpec{URI: uri}, "", "  ")
}

func (m *manager) environmentExists(name string) (bool, error) {
	envs, err := m.GetEnvironments()
	if err != nil {
		return false, err
	}

	envExists := false
	for _, env := range envs {
		if env.Name == name {
			envExists = true
			break
		}
	}

	return envExists, nil
}