Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

- [#2280](https://github.com/pulumi/pulumi-kubernetes/issues/2280) Add `enablePatchForce` provider config option to force SSA patch conflicts on a per-stack basis.

### Fixed

- Fix CRD parameterization: derive package name from CRD groups instead of hardcoding `"mycrd"`, implement `Parameterize(Value)` for subsequent runs, and flatten array-of-objects in OpenAPI specs to generate typed SDKs.

### Changed

- Upgrade Kubernetes schema and libraries to v1.35.3.
Expand Down
39 changes: 34 additions & 5 deletions provider/pkg/gen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ type schemaGenerator struct {
// listed as a language dependency. The value is the version of the Pulumi Kubernetes provider
// to depend on.
pulumiKubernetesDependency string

// pkgName overrides the package name. Defaults to "kubernetes" when empty.
pkgName string
}

type schemaGeneratorOption interface {
Expand Down Expand Up @@ -110,15 +113,37 @@ func WithPulumiKubernetesDependency(version string) schemaGeneratorOption {
return &withPulumiKubernetesDependency{pulumiVersion: version}
}

type withPackageNameOption struct {
pkgName string
}

func (o *withPackageNameOption) apply(sg *schemaGenerator) {
sg.pkgName = o.pkgName
}

// WithPackageName overrides the package name used for the generated schema.
// When set, all resource/type tokens use this name as their prefix instead of
// "kubernetes", and the Go import base path is adjusted accordingly. This is
// used by CRD parameterization so the generated SDK can coexist alongside the
// base @pulumi/kubernetes provider.
func WithPackageName(name string) schemaGeneratorOption {
return &withPackageNameOption{pkgName: name}
}

// PulumiSchema will generate a Pulumi schema for the given k8s schema.
func PulumiSchema(swagger map[string]any, opts ...schemaGeneratorOption) pschema.PackageSpec {
gen := &schemaGenerator{}
for _, o := range opts {
o.apply(gen)
}

pkgName := gen.pkgName
if pkgName == "" {
pkgName = "kubernetes"
}

pkg := pschema.PackageSpec{
Name: "kubernetes",
Name: pkgName,
Description: "A Pulumi package for creating and managing Kubernetes resources.",
DisplayName: "Kubernetes",
License: "Apache-2.0",
Expand Down Expand Up @@ -316,11 +341,11 @@ func PulumiSchema(swagger map[string]any, opts ...schemaGeneratorOption) pschema
},
"kubeClientSettings": {
Description: "Options for tuning the Kubernetes client used by a Provider.",
TypeSpec: pschema.TypeSpec{Ref: "#/types/kubernetes:index:KubeClientSettings"},
TypeSpec: pschema.TypeSpec{Ref: fmt.Sprintf("#/types/%s:index:KubeClientSettings", pkgName)},
},
"helmReleaseSettings": {
Description: "Options to configure the Helm Release resource.",
TypeSpec: pschema.TypeSpec{Ref: "#/types/kubernetes:index:HelmReleaseSettings"},
TypeSpec: pschema.TypeSpec{Ref: fmt.Sprintf("#/types/%s:index:HelmReleaseSettings", pkgName)},
},
"suppressHelmHookWarnings": {
DefaultInfo: &pschema.DefaultSpec{
Expand All @@ -344,6 +369,10 @@ func PulumiSchema(swagger map[string]any, opts ...schemaGeneratorOption) pschema
pkg.Parameterization = gen.parameterization

goImportPath := "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
if pkgName != "kubernetes" {
goIdent := strings.ReplaceAll(pkgName, "-", "")
goImportPath = fmt.Sprintf("github.com/pulumi/pulumi-kubernetes-%s/sdk/v4/go/%s", pkgName, goIdent)
}

csharpNamespaces := map[string]string{
"apiextensions": "ApiExtensions",
Expand Down Expand Up @@ -377,15 +406,15 @@ func PulumiSchema(swagger map[string]any, opts ...schemaGeneratorOption) pschema
}

definitions := swagger["definitions"].(map[string]any)
groupsSlice := createGroups(definitions, gen.allowHyphens)
groupsSlice := createGroups(definitions, gen.allowHyphens, pkgName)

for _, group := range groupsSlice {
if group.Group() == "apiserverinternal" {
continue
}
for _, version := range group.Versions() {
for _, kind := range version.Kinds() {
tok := fmt.Sprintf(`kubernetes:%s:%s`, kind.apiVersion, kind.kind)
tok := fmt.Sprintf(`%s:%s:%s`, pkgName, kind.apiVersion, kind.kind)
var patchTok string

// Generate patch variants for non-list resources.
Expand Down
33 changes: 17 additions & 16 deletions provider/pkg/gen/typegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,11 @@ const (
v1CRSubresourceStatus = apiextensionsV1 + ".CustomResourceSubresourceStatus"
)

func makeSchemaTypeSpec(prop map[string]any, canonicalGroups map[string]string) pschema.TypeSpec {
func makeSchemaTypeSpec(prop map[string]any, canonicalGroups map[string]string, pkgName string) pschema.TypeSpec {
if t, exists := prop["type"]; exists {
switch t := t.(string); t {
case "array":
elemSpec := makeSchemaTypeSpec(prop["items"].(map[string]any), canonicalGroups)
elemSpec := makeSchemaTypeSpec(prop["items"].(map[string]any), canonicalGroups, pkgName)
return pschema.TypeSpec{
Type: "array",
Items: &elemSpec,
Expand All @@ -353,7 +353,7 @@ func makeSchemaTypeSpec(prop map[string]any, canonicalGroups map[string]string)
return pschema.TypeSpec{Type: "object"}
}

elemSpec := makeSchemaTypeSpec(additionalProperties.(map[string]any), canonicalGroups)
elemSpec := makeSchemaTypeSpec(additionalProperties.(map[string]any), canonicalGroups, pkgName)
return pschema.TypeSpec{
Type: "object",
AdditionalProperties: &elemSpec,
Expand Down Expand Up @@ -399,41 +399,41 @@ func makeSchemaTypeSpec(prop map[string]any, canonicalGroups map[string]string)
return pschema.TypeSpec{Type: "string"}
case v1beta1JSONSchemaPropsOrBool:
return pschema.TypeSpec{OneOf: []pschema.TypeSpec{
{Ref: "#/types/kubernetes:apiextensions.k8s.io/v1beta1:JSONSchemaProps"},
{Ref: fmt.Sprintf("#/types/%s:apiextensions.k8s.io/v1beta1:JSONSchemaProps", pkgName)},
{Type: "boolean"},
}}
case v1JSONSchemaPropsOrBool:
return pschema.TypeSpec{OneOf: []pschema.TypeSpec{
{Ref: "#/types/kubernetes:apiextensions.k8s.io/v1:JSONSchemaProps"},
{Ref: fmt.Sprintf("#/types/%s:apiextensions.k8s.io/v1:JSONSchemaProps", pkgName)},
{Type: "boolean"},
}}
case v1beta1JSONSchemaPropsOrArray:
return pschema.TypeSpec{OneOf: []pschema.TypeSpec{
{Ref: "#/types/kubernetes:apiextensions.k8s.io/v1beta1:JSONSchemaProps"},
{Ref: fmt.Sprintf("#/types/%s:apiextensions.k8s.io/v1beta1:JSONSchemaProps", pkgName)},
{
Type: "array",
Items: &pschema.TypeSpec{Ref: "pulumi.json#/Json"},
},
}}
case v1JSONSchemaPropsOrArray:
return pschema.TypeSpec{OneOf: []pschema.TypeSpec{
{Ref: "#/types/kubernetes:apiextensions.k8s.io/v1:JSONSchemaProps"},
{Ref: fmt.Sprintf("#/types/%s:apiextensions.k8s.io/v1:JSONSchemaProps", pkgName)},
{
Type: "array",
Items: &pschema.TypeSpec{Ref: "pulumi.json#/Json"},
},
}}
case v1beta1JSONSchemaPropsOrStringArray:
return pschema.TypeSpec{OneOf: []pschema.TypeSpec{
{Ref: "#/types/kubernetes:apiextensions.k8s.io/v1beta1:JSONSchemaProps"},
{Ref: fmt.Sprintf("#/types/%s:apiextensions.k8s.io/v1beta1:JSONSchemaProps", pkgName)},
{
Type: "array",
Items: &pschema.TypeSpec{Type: "string"},
},
}}
case v1JSONSchemaPropsOrStringArray:
return pschema.TypeSpec{OneOf: []pschema.TypeSpec{
{Ref: "#/types/kubernetes:apiextensions.k8s.io/v1:JSONSchemaProps"},
{Ref: fmt.Sprintf("#/types/%s:apiextensions.k8s.io/v1:JSONSchemaProps", pkgName)},
{
Type: "array",
Items: &pschema.TypeSpec{Type: "string"},
Expand All @@ -445,14 +445,14 @@ func makeSchemaTypeSpec(prop map[string]any, canonicalGroups map[string]string)

gvk := GVKFromRef(ref)
if canonicalGroup, ok := canonicalGroups[gvk.Group]; ok {
return pschema.TypeSpec{Ref: fmt.Sprintf("#/types/kubernetes:%s/%s:%s",
canonicalGroup, gvk.Version, gvk.Kind)}
return pschema.TypeSpec{Ref: fmt.Sprintf("#/types/%s:%s/%s:%s",
pkgName, canonicalGroup, gvk.Version, gvk.Kind)}
}
panic("Canonical group not set for ref: " + ref)
}

func makeSchemaType(prop map[string]any, canonicalGroups map[string]string) string {
spec := makeSchemaTypeSpec(prop, canonicalGroups)
func makeSchemaType(prop map[string]any, canonicalGroups map[string]string, pkgName string) string {
spec := makeSchemaTypeSpec(prop, canonicalGroups, pkgName)
b, err := json.Marshal(spec)
contract.AssertNoErrorf(err, "unexpected error while marshaling JSON")
return string(b)
Expand All @@ -464,11 +464,11 @@ func makeSchemaType(prop map[string]any, canonicalGroups map[string]string) stri

// --------------------------------------------------------------------------

func createGroups(definitionsJSON map[string]any, allowHyphens bool) []GroupConfig {
func createGroups(definitionsJSON map[string]any, allowHyphens bool, pkgName string) []GroupConfig {
canonicalGroups := createCanonicalGroups(definitionsJSON)
definitions := createDefinitions(definitionsJSON, canonicalGroups)
aliases := createAliases(definitions, canonicalGroups)
kinds := createKinds(definitions, canonicalGroups, aliases, allowHyphens)
kinds := createKinds(definitions, canonicalGroups, aliases, allowHyphens, pkgName)
versions := createVersions(kinds)
groups := createGroupsFromVersions(versions)
return groups
Expand Down Expand Up @@ -575,6 +575,7 @@ func createKinds(
canonicalGroups map[string]string,
aliases map[string][]any,
allowHyphens bool,
pkgName string,
) []KindConfig {
var kinds []KindConfig

Expand Down Expand Up @@ -617,7 +618,7 @@ func createKinds(
}
}

schemaType := makeSchemaType(prop, canonicalGroups)
schemaType := makeSchemaType(prop, canonicalGroups, pkgName)

// `-` is invalid in variable names, so replace with `_`
switch propName {
Expand Down
8 changes: 4 additions & 4 deletions provider/pkg/gen/typegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestCreateGroups_IdentifyListKinds(t *testing.T) {
}
}

configGroups := createGroups(definitions, true)
configGroups := createGroups(definitions, true, "kubernetes")

// Loop through all parsed kinds and ensure they are accounted for.
for _, g := range configGroups {
Expand Down Expand Up @@ -658,7 +658,7 @@ func TestMakeSchemaTypeSpec(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := makeSchemaTypeSpec(tt.prop, tt.canonicalGroups)
actual := makeSchemaTypeSpec(tt.prop, tt.canonicalGroups, "kubernetes")
assert.Equal(t, tt.expectedTypeSpec, actual)
})
}
Expand Down Expand Up @@ -862,7 +862,7 @@ func TestCreateKinds_EmptyObjectTypes(t *testing.T) {
},
}

kinds := createKinds(definitions, map[string]string{}, map[string][]any{}, false)
kinds := createKinds(definitions, map[string]string{}, map[string][]any{}, false, "kubernetes")

kindNames := make([]string, len(kinds))
for i, k := range kinds {
Expand Down Expand Up @@ -960,7 +960,7 @@ func TestReservedPropertyNames(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kinds := createKinds([]definition{tt.definition}, map[string]string{}, map[string][]any{}, false)
kinds := createKinds([]definition{tt.definition}, map[string]string{}, map[string][]any{}, false, "kubernetes")
require.Len(t, kinds, 1)
properties := slices.Sorted(fxs.Map(kinds[0].Properties(), func(p Property) string { return p.Name() }))
assert.Equal(t, tt.expectedProperties, properties)
Expand Down
10 changes: 7 additions & 3 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2498,9 +2498,13 @@ func (k *kubeProvider) gvkFromUnstructured(input *unstructured.Unstructured) sch
}

func (k *kubeProvider) gvkFromURN(urn resource.URN) (schema.GroupVersionKind, error) {
if string(urn.Type().Package()) != k.providerPackage {
return schema.GroupVersionKind{}, fmt.Errorf("unrecognized resource type: %q for this provider",
urn.Type())
pkg := string(urn.Type().Package())
if pkg != k.providerPackage && k.crdSchemas.get(pkg, "") == nil {
// Also check if the package matches any parameterized CRD package (any version).
if !k.crdSchemas.hasPackage(pkg) {
return schema.GroupVersionKind{}, fmt.Errorf("unrecognized resource type: %q for this provider",
urn.Type())
}
}

// Emit GVK.
Expand Down
Loading
Loading