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 }