github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/tanka/load.go (about)

     1  package tanka
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/grafana/tanka/pkg/jsonnet/implementations/binary"
    10  	"github.com/grafana/tanka/pkg/jsonnet/implementations/goimpl"
    11  	"github.com/grafana/tanka/pkg/jsonnet/implementations/types"
    12  	"github.com/grafana/tanka/pkg/jsonnet/jpath"
    13  	"github.com/grafana/tanka/pkg/kubernetes"
    14  	"github.com/grafana/tanka/pkg/kubernetes/manifest"
    15  	"github.com/grafana/tanka/pkg/process"
    16  	"github.com/grafana/tanka/pkg/spec"
    17  	"github.com/grafana/tanka/pkg/spec/v1alpha1"
    18  	"github.com/pkg/errors"
    19  	"github.com/rs/zerolog/log"
    20  )
    21  
    22  // environmentExtCode is the extCode ID `tk.env` uses underneath
    23  // TODO: remove "import tk" and replace it with tanka-util
    24  const environmentExtCode = spec.APIGroup + "/environment"
    25  
    26  // Load loads the Environment at `path`. It automatically detects whether to
    27  // load inline or statically
    28  func Load(path string, opts Opts) (*LoadResult, error) {
    29  	env, err := LoadEnvironment(path, opts)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	result, err := LoadManifests(env, opts.Filters)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	// Check if there are still any inline environments in the manifests
    40  	// They are not real k8s resources, and cannot be applied
    41  	if envs := process.Filter(result.Resources, process.MustStrExps("Environment/.*")); len(envs) > 0 {
    42  		return nil, errors.New("found a tanka Environment resource. Check that you aren't using a spec.json and inline environments simultaneously")
    43  	}
    44  
    45  	return result, nil
    46  }
    47  
    48  func LoadEnvironment(path string, opts Opts) (*v1alpha1.Environment, error) {
    49  	_, err := os.Stat(path)
    50  	if os.IsNotExist(err) {
    51  		log.Info().Msgf("Path %q does not exist, trying to use it as an environment name", path)
    52  		opts.Name = path
    53  		path = "."
    54  	} else if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	loader, err := DetectLoader(path, opts)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	env, err := loader.Load(path, LoaderOpts{opts.JsonnetOpts, opts.Name})
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	return env, nil
    69  }
    70  
    71  func LoadManifests(env *v1alpha1.Environment, filters process.Matchers) (*LoadResult, error) {
    72  	if err := checkVersion(env.Spec.ExpectVersions.Tanka); err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	processed, err := process.Process(*env, filters)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	return &LoadResult{Env: env, Resources: processed}, nil
    82  }
    83  
    84  // Peek loads the metadata of the environment at path. To get resources as well,
    85  // use Load
    86  func Peek(path string, opts Opts) (*v1alpha1.Environment, error) {
    87  	loader, err := DetectLoader(path, opts)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	return loader.Peek(path, LoaderOpts{opts.JsonnetOpts, opts.Name})
    93  }
    94  
    95  // List finds metadata of all environments at path that could possibly be
    96  // loaded. List can be used to deal with multiple inline environments, by first
    97  // listing them, choosing the right one and then only loading that one
    98  func List(path string, opts Opts) ([]*v1alpha1.Environment, error) {
    99  	loader, err := DetectLoader(path, opts)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	return loader.List(path, LoaderOpts{opts.JsonnetOpts, opts.Name})
   105  }
   106  
   107  func getJsonnetImplementation(path string, opts Opts) (types.JsonnetImplementation, error) {
   108  	if strings.HasPrefix(opts.JsonnetImplementation, "binary:") {
   109  		binPath := strings.TrimPrefix(opts.JsonnetImplementation, "binary:")
   110  
   111  		// check if binary exists and is executable
   112  		stat, err := os.Stat(binPath)
   113  		if err != nil {
   114  			return nil, fmt.Errorf("binary %q does not exist", binPath)
   115  		}
   116  		if stat.Mode()&0111 == 0 {
   117  			return nil, fmt.Errorf("binary %q is not executable", binPath)
   118  		}
   119  
   120  		return &binary.JsonnetBinaryImplementation{
   121  			BinPath: binPath,
   122  		}, nil
   123  	}
   124  
   125  	switch opts.JsonnetImplementation {
   126  	case "go", "":
   127  		return &goimpl.JsonnetGoImplementation{
   128  			Path: path,
   129  		}, nil
   130  	default:
   131  		return nil, fmt.Errorf("unknown jsonnet implementation: %s", opts.JsonnetImplementation)
   132  	}
   133  }
   134  
   135  // Eval returns the raw evaluated Jsonnet
   136  func Eval(path string, opts Opts) (interface{}, error) {
   137  	loader, err := DetectLoader(path, opts)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	return loader.Eval(path, LoaderOpts{opts.JsonnetOpts, opts.Name})
   143  }
   144  
   145  // DetectLoader detects whether the environment is inline or static and picks
   146  // the approriate loader
   147  func DetectLoader(path string, opts Opts) (Loader, error) {
   148  	_, base, err := jpath.Dirs(path)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	jsonnetImpl, err := getJsonnetImplementation(base, opts)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	// check if spec.json exists
   159  	_, err = os.Stat(filepath.Join(base, spec.Specfile))
   160  	if os.IsNotExist(err) {
   161  		return &InlineLoader{
   162  			jsonnetImpl: jsonnetImpl,
   163  		}, nil
   164  	} else if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	return &StaticLoader{
   169  		jsonnetImpl: jsonnetImpl,
   170  	}, nil
   171  }
   172  
   173  // Loader is an abstraction over the process of loading Environments
   174  type Loader interface {
   175  	// Load a single environment at path
   176  	Load(path string, opts LoaderOpts) (*v1alpha1.Environment, error)
   177  
   178  	// Peek only loads metadata and omits the actual resources
   179  	Peek(path string, opts LoaderOpts) (*v1alpha1.Environment, error)
   180  
   181  	// List returns metadata of all possible environments at path that can be
   182  	// loaded
   183  	List(path string, opts LoaderOpts) ([]*v1alpha1.Environment, error)
   184  
   185  	// Eval returns the raw evaluated Jsonnet
   186  	Eval(path string, opts LoaderOpts) (interface{}, error)
   187  }
   188  
   189  type LoaderOpts struct {
   190  	JsonnetOpts
   191  	Name string
   192  }
   193  
   194  type LoadResult struct {
   195  	Env       *v1alpha1.Environment
   196  	Resources manifest.List
   197  }
   198  
   199  func (l LoadResult) Connect() (*kubernetes.Kubernetes, error) {
   200  	env := *l.Env
   201  
   202  	// check env is complete
   203  	s := ""
   204  	if env.Spec.APIServer == "" && len(env.Spec.ContextNames) < 1 {
   205  		s += "  * spec.apiServer|spec.contextNames: No Kubernetes cluster endpoint or context names specified. Please specify only one."
   206  	} else if env.Spec.APIServer != "" && len(env.Spec.ContextNames) > 0 {
   207  		s += "  * spec.apiServer|spec.contextNames: These fields are mutually exclusive, please only specify one."
   208  	}
   209  	if env.Spec.Namespace == "" {
   210  		s += "  * spec.namespace: Default namespace missing"
   211  	}
   212  	if s != "" {
   213  		return nil, fmt.Errorf("your Environment's spec.json seems incomplete:\n%s\n\nPlease see https://tanka.dev/config for reference", s)
   214  	}
   215  
   216  	// connect client
   217  	kube, err := kubernetes.New(env)
   218  	if err != nil {
   219  		return nil, errors.Wrap(err, "connecting to Kubernetes")
   220  	}
   221  
   222  	return kube, nil
   223  }