github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/specs/v2.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package specs
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/errors"
    11  	admissionregistration "k8s.io/api/admissionregistration/v1beta1"
    12  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    13  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    14  
    15  	"github.com/juju/juju/caas/specs"
    16  )
    17  
    18  type k8sContainerV2 struct {
    19  	specs.ContainerSpecV2 `json:",inline" yaml:",inline"`
    20  	Kubernetes            *K8sContainerSpec `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"`
    21  }
    22  
    23  // Validate validates k8sContainerV2.
    24  func (c *k8sContainerV2) Validate() error {
    25  	if err := c.ContainerSpecV2.Validate(); err != nil {
    26  		return errors.Trace(err)
    27  	}
    28  	if c.Kubernetes != nil {
    29  		if err := c.Kubernetes.Validate(); err != nil {
    30  			return errors.Trace(err)
    31  		}
    32  	}
    33  	return nil
    34  }
    35  
    36  func fileSetsV2ToFileSets(fs []specs.FileSetV2) (out []specs.FileSet) {
    37  	for _, f := range fs {
    38  		newf := specs.FileSet{
    39  			Name:      f.Name,
    40  			MountPath: f.MountPath,
    41  		}
    42  
    43  		// We sort the keys of the files here to get a deterministic ordering.
    44  		// See lp:1895598
    45  		keys := specs.SortKeysForFiles(f.Files)
    46  
    47  		for _, k := range keys {
    48  			newf.Files = append(newf.Files, specs.File{
    49  				Path:    k,
    50  				Content: f.Files[k],
    51  			})
    52  		}
    53  		out = append(out, newf)
    54  	}
    55  	return out
    56  }
    57  
    58  func (c *k8sContainerV2) ToContainerSpec() specs.ContainerSpec {
    59  	result := specs.ContainerSpec{
    60  		ImageDetails:    c.ImageDetails,
    61  		Name:            c.Name,
    62  		Init:            c.Init,
    63  		Image:           c.Image,
    64  		Ports:           c.Ports,
    65  		Command:         c.Command,
    66  		Args:            c.Args,
    67  		WorkingDir:      c.WorkingDir,
    68  		EnvConfig:       c.Config,
    69  		VolumeConfig:    fileSetsV2ToFileSets(c.Files),
    70  		ImagePullPolicy: c.ImagePullPolicy,
    71  	}
    72  	if c.Kubernetes != nil {
    73  		result.ProviderContainer = c.Kubernetes
    74  	}
    75  	return result
    76  }
    77  
    78  type k8sContainersV2 struct {
    79  	Containers []k8sContainerV2 `json:"containers" yaml:"containers"`
    80  }
    81  
    82  // Validate is defined on ProviderContainer.
    83  func (cs *k8sContainersV2) Validate() error {
    84  	if len(cs.Containers) == 0 {
    85  		return errors.New("require at least one container spec")
    86  	}
    87  	for _, c := range cs.Containers {
    88  		if err := c.Validate(); err != nil {
    89  			return errors.Trace(err)
    90  		}
    91  	}
    92  	return nil
    93  }
    94  
    95  type caaSSpecV2 = specs.PodSpecV2
    96  
    97  type podSpecV2 struct {
    98  	caaSSpecV2      `json:",inline" yaml:",inline"`
    99  	K8sPodSpecV2    `json:",inline" yaml:",inline"`
   100  	k8sContainersV2 `json:",inline" yaml:",inline"`
   101  }
   102  
   103  // Validate is defined on ProviderPod.
   104  func (p podSpecV2) Validate() error {
   105  	if err := p.K8sPodSpecV2.Validate(); err != nil {
   106  		return errors.Trace(err)
   107  	}
   108  	if err := p.k8sContainersV2.Validate(); err != nil {
   109  		return errors.Trace(err)
   110  	}
   111  	return nil
   112  }
   113  
   114  func (p podSpecV2) ToLatest() *specs.PodSpec {
   115  	pSpec := &specs.PodSpec{}
   116  	pSpec.Version = specs.CurrentVersion
   117  	// TODO(caas): OmitServiceFrontend is deprecated in v2 and will be removed in a later version.
   118  	pSpec.OmitServiceFrontend = false
   119  	for _, c := range p.Containers {
   120  		pSpec.Containers = append(pSpec.Containers, c.ToContainerSpec())
   121  	}
   122  	pSpec.Service = p.caaSSpecV2.Service
   123  	pSpec.ConfigMaps = p.caaSSpecV2.ConfigMaps
   124  
   125  	if p.caaSSpecV2.ServiceAccount != nil {
   126  		pSpec.ServiceAccount = p.caaSSpecV2.ServiceAccount.ToLatest()
   127  	}
   128  	if p.K8sPodSpecV2.KubernetesResources != nil {
   129  		pSpec.ProviderPod = &K8sPodSpec{
   130  			KubernetesResources: p.K8sPodSpecV2.KubernetesResources.toLatest(),
   131  		}
   132  	}
   133  	return pSpec
   134  }
   135  
   136  // K8sPodSpecV2 is a subset of v1.PodSpec which defines
   137  // attributes we expose for charms to set.
   138  type K8sPodSpecV2 struct {
   139  	// k8s resources.
   140  	KubernetesResources *KubernetesResourcesV2 `json:"kubernetesResources,omitempty" yaml:"kubernetesResources,omitempty"`
   141  }
   142  
   143  // Validate is defined on ProviderPod.
   144  func (p *K8sPodSpecV2) Validate() error {
   145  	if p.KubernetesResources != nil {
   146  		if err := p.KubernetesResources.Validate(); err != nil {
   147  			return errors.Trace(err)
   148  		}
   149  	}
   150  	return nil
   151  }
   152  
   153  // K8sServiceAccountSpecV2 defines spec for referencing or creating a service account for version 2.
   154  type K8sServiceAccountSpecV2 struct {
   155  	Name                       string `json:"name" yaml:"name"`
   156  	specs.ServiceAccountSpecV2 `json:",inline" yaml:",inline"`
   157  }
   158  
   159  func (ksa K8sServiceAccountSpecV2) toLatest() K8sRBACResources {
   160  	o := ksa.ServiceAccountSpecV2.ToLatest()
   161  	o.SetName(ksa.Name)
   162  	return K8sRBACResources{
   163  		ServiceAccounts: []K8sServiceAccountSpec{
   164  			{
   165  				Name: o.GetName(),
   166  				ServiceAccountSpecV3: specs.ServiceAccountSpecV3{
   167  					AutomountServiceAccountToken: o.AutomountServiceAccountToken,
   168  					Roles:                        o.Roles,
   169  				},
   170  			},
   171  		},
   172  	}
   173  }
   174  
   175  // Validate returns an error if the spec is not valid.
   176  func (ksa K8sServiceAccountSpecV2) Validate() error {
   177  	if ksa.Name == "" {
   178  		return errors.New("service account name is missing")
   179  	}
   180  	return errors.Trace(ksa.ServiceAccountSpecV2.Validate())
   181  }
   182  
   183  // KubernetesResourcesV2 is the k8s related resources for version 2.
   184  type KubernetesResourcesV2 struct {
   185  	Pod *PodSpec `json:"pod,omitempty" yaml:"pod,omitempty"`
   186  
   187  	Secrets                   []K8sSecret                                                  `json:"secrets" yaml:"secrets"`
   188  	CustomResourceDefinitions map[string]apiextensionsv1beta1.CustomResourceDefinitionSpec `json:"customResourceDefinitions,omitempty" yaml:"customResourceDefinitions,omitempty"`
   189  	CustomResources           map[string][]unstructured.Unstructured                       `json:"customResources,omitempty" yaml:"customResources,omitempty"`
   190  
   191  	MutatingWebhookConfigurations   map[string][]admissionregistration.MutatingWebhook   `json:"mutatingWebhookConfigurations,omitempty" yaml:"mutatingWebhookConfigurations,omitempty"`
   192  	ValidatingWebhookConfigurations map[string][]admissionregistration.ValidatingWebhook `json:"validatingWebhookConfigurations,omitempty" yaml:"validatingWebhookConfigurations,omitempty"`
   193  
   194  	ServiceAccounts  []K8sServiceAccountSpecV2 `json:"serviceAccounts,omitempty" yaml:"serviceAccounts,omitempty"`
   195  	IngressResources []K8sIngress              `json:"ingressResources,omitempty" yaml:"ingressResources,omitempty"`
   196  }
   197  
   198  func validateCustomResourceDefinitionV2(name string, crd apiextensionsv1beta1.CustomResourceDefinitionSpec) error {
   199  	if crd.Scope != apiextensionsv1beta1.NamespaceScoped && crd.Scope != apiextensionsv1beta1.ClusterScoped {
   200  		return errors.NewNotSupported(nil,
   201  			fmt.Sprintf("custom resource definition %q scope %q is not supported, please use %q or %q scope",
   202  				name, crd.Scope, apiextensionsv1beta1.NamespaceScoped, apiextensionsv1beta1.ClusterScoped),
   203  		)
   204  	}
   205  	return nil
   206  }
   207  
   208  func (krs *KubernetesResourcesV2) toLatest() *KubernetesResources {
   209  	out := &KubernetesResources{
   210  		Pod:                       krs.Pod,
   211  		Secrets:                   krs.Secrets,
   212  		CustomResourceDefinitions: customResourceDefinitionsToLatest(krs.CustomResourceDefinitions),
   213  		CustomResources:           krs.CustomResources,
   214  		IngressResources:          krs.IngressResources,
   215  	}
   216  	for _, sa := range krs.ServiceAccounts {
   217  		rbacSources := sa.toLatest()
   218  		out.ServiceAccounts = append(out.ServiceAccounts, rbacSources.ServiceAccounts...)
   219  	}
   220  	for name, webhooks := range krs.MutatingWebhookConfigurations {
   221  		out.MutatingWebhookConfigurations = append(out.MutatingWebhookConfigurations, K8sMutatingWebhook{
   222  			Meta:     Meta{Name: name},
   223  			Webhooks: mutatingWebhookFromV1Beta1(webhooks),
   224  		})
   225  	}
   226  	for name, webhooks := range krs.ValidatingWebhookConfigurations {
   227  		out.ValidatingWebhookConfigurations = append(out.ValidatingWebhookConfigurations, K8sValidatingWebhook{
   228  			Meta:     Meta{Name: name},
   229  			Webhooks: validatingWebhookFromV1Beta1(webhooks),
   230  		})
   231  	}
   232  	return out
   233  }
   234  
   235  func customResourceDefinitionsToLatest(crds map[string]apiextensionsv1beta1.CustomResourceDefinitionSpec) (out []K8sCustomResourceDefinition) {
   236  	for name, crd := range crds {
   237  		out = append(out, K8sCustomResourceDefinition{
   238  			Meta: Meta{Name: name},
   239  			Spec: K8sCustomResourceDefinitionSpec{
   240  				Version:     K8sCustomResourceDefinitionV1Beta1,
   241  				SpecV1Beta1: crd,
   242  			},
   243  		})
   244  	}
   245  	return out
   246  }
   247  
   248  // Validate is defined on ProviderPod.
   249  func (krs *KubernetesResourcesV2) Validate() error {
   250  	for k, crd := range krs.CustomResourceDefinitions {
   251  		if err := validateCustomResourceDefinitionV2(k, crd); err != nil {
   252  			return errors.Trace(err)
   253  		}
   254  	}
   255  
   256  	for k, webhooks := range krs.MutatingWebhookConfigurations {
   257  		if len(webhooks) == 0 {
   258  			return errors.NotValidf("empty webhooks %q", k)
   259  		}
   260  	}
   261  	for k, webhooks := range krs.ValidatingWebhookConfigurations {
   262  		if len(webhooks) == 0 {
   263  			return errors.NotValidf("empty webhooks %q", k)
   264  		}
   265  	}
   266  
   267  	for _, sa := range krs.ServiceAccounts {
   268  		if err := sa.Validate(); err != nil {
   269  			return errors.Trace(err)
   270  		}
   271  	}
   272  
   273  	for _, ing := range krs.IngressResources {
   274  		if err := ing.Validate(); err != nil {
   275  			return errors.Trace(err)
   276  		}
   277  	}
   278  	return nil
   279  }
   280  
   281  func parsePodSpecV2(in string) (_ PodSpecConverter, err error) {
   282  	var spec podSpecV2
   283  	decoder := newStrictYAMLOrJSONDecoder(strings.NewReader(in), len(in))
   284  	if err = decoder.Decode(&spec); err != nil {
   285  		return nil, errors.Trace(err)
   286  	}
   287  	return &spec, nil
   288  }