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 }