github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/specs/builder.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package specs 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "io" 11 "sync" 12 13 "github.com/juju/errors" 14 "github.com/kr/pretty" 15 k8serrors "k8s.io/apimachinery/pkg/api/errors" 16 "k8s.io/apimachinery/pkg/api/meta" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 19 "k8s.io/apimachinery/pkg/runtime" 20 apischema "k8s.io/apimachinery/pkg/runtime/schema" 21 "k8s.io/apimachinery/pkg/types" 22 "k8s.io/apimachinery/pkg/util/yaml" 23 "k8s.io/client-go/discovery" 24 "k8s.io/client-go/discovery/cached/memory" 25 "k8s.io/client-go/kubernetes/scheme" 26 "k8s.io/client-go/rest" 27 "k8s.io/client-go/restmapper" 28 29 "github.com/juju/juju/caas" 30 k8sannotations "github.com/juju/juju/core/annotations" 31 ) 32 33 var ( 34 codec = unstructured.UnstructuredJSONScheme 35 metadataAccessor = meta.NewAccessor() 36 ) 37 38 func processRawData(data []byte, defaults *apischema.GroupVersionKind, into runtime.Object) (obj runtime.Object, gvk *apischema.GroupVersionKind, err error) { 39 obj, gvk, err = codec.Decode(data, defaults, into) 40 if err != nil { 41 return obj, gvk, errors.Trace(err) 42 } 43 44 if _, ok := obj.(runtime.Unstructured); !ok { 45 return obj, gvk, errors.Trace(nil) 46 } 47 48 return obj, gvk, nil 49 } 50 51 type resourceInfo struct { 52 name string 53 namespace string 54 resourceVersion string 55 content *runtime.RawExtension 56 57 mapping *meta.RESTMapping 58 client rest.Interface 59 } 60 61 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/meta_mock.go k8s.io/apimachinery/pkg/api/meta RESTMapper 62 func getRestMapper(c rest.Interface) meta.RESTMapper { 63 discoveryClient := discovery.NewDiscoveryClient(c) 64 mapper := restmapper.NewDeferredDiscoveryRESTMapper( 65 memory.NewMemCacheClient(discoveryClient), 66 ) 67 return restmapper.NewShortcutExpander(mapper, discoveryClient, func(warning string) { 68 logger.Warningf(warning) 69 }) 70 } 71 72 func (ri *resourceInfo) withNamespace(namespace string) *resourceInfo { 73 if ri.namespace != "" && ri.namespace != namespace { 74 logger.Debugf("namespace is force set from %q to %q", ri.namespace, namespace) 75 } 76 ri.namespace = namespace 77 _ = metadataAccessor.SetNamespace(ri.content.Object, ri.namespace) 78 return ri 79 } 80 81 func (ri *resourceInfo) ensureLabels(labels map[string]string) error { 82 providedLabels, err := metadataAccessor.Labels(ri.content.Object) 83 if err != nil { 84 return errors.Trace(err) 85 } 86 if len(providedLabels) == 0 { 87 providedLabels = make(map[string]string) 88 } 89 for k, v := range labels { 90 providedLabels[k] = v 91 } 92 return metadataAccessor.SetLabels(ri.content.Object, providedLabels) 93 } 94 95 func (ri *resourceInfo) ensureAnnotations(annoations k8sannotations.Annotation) error { 96 providedAnnoations, err := metadataAccessor.Annotations(ri.content.Object) 97 if err != nil { 98 return errors.Trace(err) 99 } 100 return metadataAccessor.SetAnnotations( 101 ri.content.Object, k8sannotations.New(providedAnnoations).Merge(annoations).ToMap(), 102 ) 103 } 104 105 func getWorkloadResourceType(t caas.DeploymentType) string { 106 switch t { 107 case caas.DeploymentDaemon: 108 return "daemonsets" 109 case caas.DeploymentStateless: 110 return "deployments" 111 case caas.DeploymentStateful: 112 return "statefulsets" 113 default: 114 return "deployments" 115 } 116 } 117 118 type deployer struct { 119 deploymentName string 120 namespace string 121 spec string 122 workloadResourceType string 123 cfg *rest.Config 124 labelGetter func(isNamespaced bool) map[string]string 125 annotations k8sannotations.Annotation 126 newRestClient NewK8sRestClientFunc 127 128 resources []resourceInfo 129 130 restMapperGetter func(c rest.Interface) meta.RESTMapper 131 } 132 133 // DeployerInterface defines method to deploy a raw k8s spec. 134 type DeployerInterface interface { 135 Deploy(context.Context, string, bool) error 136 } 137 138 // NewK8sRestClientFunc defines a function which returns a k8s rest client based on the supplied config. 139 type NewK8sRestClientFunc func(c *rest.Config) (rest.Interface, error) 140 141 // New constructs deployer interface. 142 func New( 143 deploymentName string, 144 namespace string, 145 deploymentParams caas.DeploymentParams, 146 cfg *rest.Config, 147 labelGetter func(isNamespaced bool) map[string]string, 148 annotations k8sannotations.Annotation, 149 newRestClient NewK8sRestClientFunc, 150 ) DeployerInterface { 151 // TODO(caas): disable scale or parse the unstructuredJSON further to set workload resource replicas. 152 return newDeployer( 153 deploymentName, namespace, 154 deploymentParams, cfg, labelGetter, annotations, 155 newRestClient, getRestMapper, 156 ) 157 } 158 159 func newDeployer( 160 deploymentName string, 161 namespace string, 162 deploymentParams caas.DeploymentParams, 163 cfg *rest.Config, 164 labelGetter func(isNamespaced bool) map[string]string, 165 annotations k8sannotations.Annotation, 166 newRestClient NewK8sRestClientFunc, 167 restMapperGetter func(c rest.Interface) meta.RESTMapper, 168 ) DeployerInterface { 169 return &deployer{ 170 deploymentName: deploymentName, 171 namespace: namespace, 172 workloadResourceType: getWorkloadResourceType(deploymentParams.DeploymentType), 173 cfg: cfg, 174 labelGetter: labelGetter, 175 annotations: annotations, 176 newRestClient: newRestClient, 177 restMapperGetter: restMapperGetter, 178 } 179 } 180 181 func (d *deployer) validate() error { 182 if len(d.namespace) == 0 { 183 return errors.NotValidf("namespace is required") 184 } 185 if len(d.workloadResourceType) == 0 { 186 return errors.NotValidf("workloadResourceType is required") 187 } 188 if d.cfg == nil { 189 return errors.NotValidf("empty k8s config") 190 } 191 if d.labelGetter == nil { 192 return errors.NotValidf("labelGetter is required") 193 } 194 if d.newRestClient == nil { 195 return errors.NotValidf("newRestClient is required") 196 } 197 198 if err := d.load(); err != nil { 199 return errors.Trace(err) 200 } 201 if err := d.validateWorkload(); err != nil { 202 return errors.Trace(err) 203 } 204 // TODO(caas): check if service resource type matches the raw service spec. 205 // TODO(caas): get the API scheme and do validation further. 206 return nil 207 } 208 209 // Deploy deploys raw k8s spec to the cluster. 210 func (d *deployer) Deploy(ctx context.Context, spec string, force bool) error { 211 d.spec = spec 212 213 if err := d.validate(); err != nil { 214 return errors.Trace(err) 215 } 216 var wg sync.WaitGroup 217 wg.Add(len(d.resources)) 218 219 errChan := make(chan error) 220 done := make(chan struct{}) 221 go func() { 222 wg.Wait() 223 close(done) 224 }() 225 226 for _, r := range d.resources { 227 info := r 228 go func() { _ = d.apply(ctx, &wg, info, force, errChan) }() 229 } 230 231 for { 232 select { 233 case err := <-errChan: 234 if err != nil { 235 return errors.Trace(err) 236 } 237 case <-done: 238 return nil 239 } 240 } 241 } 242 243 func (d *deployer) validateWorkload() error { 244 for _, resource := range d.resources { 245 if resource.mapping.Resource.Resource == d.workloadResourceType { 246 return nil 247 } 248 } 249 return errors.NotValidf("empty %q resource definition", d.workloadResourceType) 250 } 251 252 func setConfigDefaults(config *rest.Config) { 253 if config.ContentConfig.NegotiatedSerializer == nil { 254 config.ContentConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 255 } 256 if len(config.UserAgent) == 0 { 257 config.UserAgent = rest.DefaultKubernetesUserAgent() 258 } 259 } 260 261 func (d *deployer) clientWithGroupVersion(gv apischema.GroupVersion) (rest.Interface, error) { 262 cfg := rest.CopyConfig(d.cfg) 263 setConfigDefaults(cfg) 264 265 cfg.APIPath = "/apis" 266 if len(gv.Group) == 0 { 267 cfg.APIPath = "/api" 268 } 269 cfg.GroupVersion = &gv 270 271 logger.Debugf("constructing rest client for resource %s for %q", pretty.Sprint(cfg.GroupVersion), d.deploymentName) 272 return d.newRestClient(cfg) 273 } 274 275 // load parses the raw k8s spec into a slice of resource info. 276 func (d *deployer) load() (err error) { 277 defer func() { 278 logger.Debugf("processing %d resources for %q, err -> %#v", len(d.resources), d.deploymentName, err) 279 }() 280 281 d.resources = []resourceInfo{} 282 283 decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(d.spec), len(d.spec)) 284 for { 285 ext := &runtime.RawExtension{} 286 if err = decoder.Decode(ext); err != nil { 287 if err == io.EOF { 288 return nil 289 } 290 return errors.Trace(err) 291 } 292 ext.Raw = bytes.TrimSpace(ext.Raw) 293 if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) { 294 continue 295 } 296 297 var gvk *apischema.GroupVersionKind 298 ext.Object, gvk, err = processRawData(ext.Raw, nil, nil) 299 if err != nil { 300 return errors.Trace(err) 301 } 302 303 item := resourceInfo{ 304 content: ext, 305 } 306 307 item.name, err = metadataAccessor.Name(item.content.Object) 308 if err != nil { 309 return errors.Trace(err) 310 } 311 312 item.namespace, err = metadataAccessor.Namespace(item.content.Object) 313 if err != nil { 314 return errors.Trace(err) 315 } 316 317 item.resourceVersion, err = metadataAccessor.ResourceVersion(item.content.Object) 318 if err != nil { 319 return errors.Trace(err) 320 } 321 322 if item.client, err = d.clientWithGroupVersion(gvk.GroupVersion()); err != nil { 323 return errors.Trace(err) 324 } 325 item.mapping, err = d.restMapperGetter(item.client).RESTMapping(gvk.GroupKind(), gvk.Version) 326 logger.Tracef("gvk.GroupKind() %s, gvk.Version %q, item.mapping %s", pretty.Sprint(gvk.GroupKind()), gvk.Version, pretty.Sprint(item.mapping)) 327 if err != nil { 328 return errors.Trace(err) 329 } 330 331 d.resources = append(d.resources, item) 332 } 333 } 334 335 // apply deploys the resource info to the k8s cluster. 336 func (d deployer) apply(ctx context.Context, wg *sync.WaitGroup, info resourceInfo, force bool, errChan chan<- error) (err error) { 337 defer wg.Done() 338 339 defer func() { 340 if err != nil { 341 select { 342 case errChan <- err: 343 default: 344 } 345 } 346 }() 347 348 isNameSpaced := info.mapping.Scope.Name() == meta.RESTScopeNameNamespace 349 350 // Ensures namespace is set. 351 _ = info.withNamespace(d.namespace) 352 // Ensure Juju labels are set. 353 if err = info.ensureLabels(d.labelGetter(isNameSpaced)); err != nil { 354 return errors.Trace(err) 355 } 356 // Ensure annotations are set. 357 if err = info.ensureAnnotations(d.annotations); err != nil { 358 return errors.Trace(err) 359 } 360 361 var data []byte 362 data, err = runtime.Encode(codec, info.content.Object) 363 if err != nil { 364 return errors.Trace(err) 365 } 366 options := &metav1.PatchOptions{ 367 Force: &force, 368 FieldManager: "juju", 369 } 370 371 doRequest := func(r *rest.Request) error { 372 err := r.NamespaceIfScoped(info.namespace, isNameSpaced). 373 Resource(info.mapping.Resource.Resource). 374 Name(info.name). 375 VersionedParams(options, metav1.ParameterCodec). 376 Body(data). 377 Do(ctx). 378 Error() 379 errMsg := fmt.Sprintf("resource %s/%s in namespace %q", info.mapping.GroupVersionKind.Kind, info.name, d.namespace) 380 if k8serrors.IsNotFound(err) { 381 return errors.NotFoundf(errMsg) 382 } 383 if k8serrors.IsAlreadyExists(err) { 384 return errors.AlreadyExistsf(errMsg) 385 } 386 return errors.Trace(err) 387 } 388 389 err = doRequest(info.client.Patch(types.ApplyPatchType)) 390 if errors.IsNotFound(err) { 391 err = doRequest(info.client.Post()) 392 } 393 return errors.Trace(err) 394 }