From 8de107ccc20ac6c0e0bac3fd58b0660a8a3385cb Mon Sep 17 00:00:00 2001 From: adohe Date: Wed, 11 Dec 2024 13:27:52 +0800 Subject: [PATCH] feat: add resources pkg to improve module development experience --- go.mod | 1 + go.sum | 2 + pkg/module/util.go | 1 + pkg/resources/id.go | 18 +++ pkg/resources/kubernetes/kubernetes.go | 60 ++++++++++ pkg/resources/terraform/terraform.go | 150 +++++++++++++++++++++++++ pkg/resources/terraform/types.go | 50 +++++++++ 7 files changed, 282 insertions(+) create mode 100644 pkg/resources/id.go create mode 100644 pkg/resources/kubernetes/kubernetes.go create mode 100644 pkg/resources/terraform/terraform.go create mode 100644 pkg/resources/terraform/types.go diff --git a/go.mod b/go.mod index 7a3cadc..30a4602 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/bytedance/mockey v1.2.10 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.1 + github.com/hashicorp/terraform-svchost v0.1.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.5 github.com/shirou/gopsutil/v4 v4.24.11 diff --git a/go.sum b/go.sum index 19e1252..3ca39d5 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= diff --git a/pkg/module/util.go b/pkg/module/util.go index fa44bd4..76315e4 100644 --- a/pkg/module/util.go +++ b/pkg/module/util.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" v1 "kusionstack.io/kusion-api-go/api.kusion.io/v1" + "kusionstack.io/kusion-module-framework/pkg/log" ) diff --git a/pkg/resources/id.go b/pkg/resources/id.go new file mode 100644 index 0000000..990317f --- /dev/null +++ b/pkg/resources/id.go @@ -0,0 +1,18 @@ +// Copyright 2024 KusionStack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resources + +// SegmentSeparator is the separator between segments in Kusion resource ID. +const SegmentSeparator = ":" diff --git a/pkg/resources/kubernetes/kubernetes.go b/pkg/resources/kubernetes/kubernetes.go new file mode 100644 index 0000000..1338017 --- /dev/null +++ b/pkg/resources/kubernetes/kubernetes.go @@ -0,0 +1,60 @@ +// Copyright 2024 KusionStack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubernetes + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + v1 "kusionstack.io/kusion-api-go/api.kusion.io/v1" + + "kusionstack.io/kusion-module-framework/pkg/resources" +) + +// ToKusionResourceID returns the Kusion resource ID for the given Kubernetes object specified by +// its GroupVersionKind and ObjectMeta. +func ToKusionResourceID(gvk schema.GroupVersionKind, objectMeta metav1.ObjectMeta) string { + // resource id example: apps/v1:Deployment:nginx:nginx-deployment + if gvk.Group == "" { + gvk.Group = "core" + } + + id := gvk.Group + resources.SegmentSeparator + gvk.Version + resources.SegmentSeparator + gvk.Kind + if objectMeta.Namespace != "" { + id += resources.SegmentSeparator + objectMeta.Namespace + } + id += resources.SegmentSeparator + objectMeta.Name + return id +} + +// NewKusionResource creates a Kusion Resource object with the given obj and objectMeta. +func NewKusionResource(obj runtime.Object, objectMeta metav1.ObjectMeta) (*v1.Resource, error) { + // TODO: this function converts int to int64 by default + unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + + gvk := obj.GetObjectKind().GroupVersionKind() + return &v1.Resource{ + ID: ToKusionResourceID(gvk, objectMeta), + Type: v1.Kubernetes, + Attributes: unstructured, + DependsOn: nil, + Extensions: map[string]any{ + v1.ResourceExtensionGVK: gvk.String(), + }, + }, nil +} diff --git a/pkg/resources/terraform/terraform.go b/pkg/resources/terraform/terraform.go new file mode 100644 index 0000000..1d47336 --- /dev/null +++ b/pkg/resources/terraform/terraform.go @@ -0,0 +1,150 @@ +// Copyright 2024 KusionStack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package terraform + +import ( + "errors" + "fmt" + "strings" + + svchost "github.com/hashicorp/terraform-svchost" + v1 "kusionstack.io/kusion-api-go/api.kusion.io/v1" + + "kusionstack.io/kusion-module-framework/pkg/resources" +) + +// DefaultProviderRegistryHost is the hostname used for provider addresses that do +// not have an explicit hostname. +const DefaultProviderRegistryHost = "registry.terraform.io" + +var ( + // errInvalidSource means provider's source is invalid, which must be in [/]/ format + errInvalidSource = errors.New(`invalid provider source string, must be in the format "[hostname/]namespace/name"`) + // errInvalidVersion means provider's version constraint is invalid, which must not be empty. + errInvalidVersion = errors.New("invalid provider version constraint") + // errInvalidResourceTypeOrName resourceType or resourceName is invalid, which must not be empty. + errInvalidResourceTypeOrName = errors.New("resourceType or resourceName is empty") +) + +// NewProvider constructs a provider instance from given source, version and configuration arguments. +func NewProvider(providerConfigs map[string]any, source, version string) (Provider, error) { + var ret Provider + + if version == "" { + return ret, errInvalidVersion + } + + _, err := parseProviderSourceString(source) + if err != nil { + return ret, err + } + + ret.Source = source + ret.Version = version + ret.ProviderConfigs = providerConfigs + return ret, nil +} + +// ToKusionResourceID takes provider, resource info and returns string representing Kusion qualified resource ID. +func ToKusionResourceID(p Provider, resourceType, resourceName string) (string, error) { + if p.Version == "" { + return "", errInvalidVersion + } + if resourceType == "" || resourceName == "" { + return "", errInvalidResourceTypeOrName + } + tfProvider, err := parseProviderSourceString(p.Source) + if err != nil { + return "", err + } + + tfProviderStr := tfProvider.String() + return strings.Join([]string{tfProviderStr, resourceType, resourceName}, resources.SegmentSeparator), nil +} + +// NewKusionResource creates a Kusion Resource object with the given resourceType, resourceID, attributes. +func NewKusionResource(p Provider, resourceType, resourceID string, + attrs map[string]interface{}, dependsOn []string, +) (*v1.Resource, error) { + if resourceType == "" { + return nil, errInvalidResourceTypeOrName + } + + // put provider info into extensions + extensions := make(map[string]interface{}, 3) + extensions["provider"] = p.String() + extensions["providerMeta"] = p.ProviderConfigs + extensions["resourceType"] = resourceType + + return &v1.Resource{ + ID: resourceID, + Type: v1.Terraform, + Attributes: attrs, + DependsOn: dependsOn, + Extensions: extensions, + }, nil +} + +// parseProviderSourceString parses the source attribute and returns a terraform provider. +// +// The following are valid source string formats: +// +// name +// namespace/name +// hostname/namespace/name +func parseProviderSourceString(str string) (TFProvider, error) { + var ret TFProvider + + // split the source string into individual components + parts := strings.Split(str, "/") + if len(parts) == 0 || len(parts) > 3 { + return ret, errInvalidSource + } + + // check for an invalid empty string in any part + for i := range parts { + if parts[i] == "" { + return ret, errInvalidSource + } + } + + // check the 'name' portion, which is always the last part + givenName := parts[len(parts)-1] + ret.Type = givenName + ret.Hostname = DefaultProviderRegistryHost + + if len(parts) == 1 { + ret.Namespace = "hashicorp" + return ret, nil + } + + if len(parts) >= 2 { + // the namespace is always the second-to-last part + givenNamespace := parts[len(parts)-2] + ret.Namespace = givenNamespace + } + + // Final Case: 3 parts + if len(parts) == 3 { + // the namespace is always the first part in a three-part source string + hn, err := svchost.ForComparison(parts[0]) + if err != nil { + return ret, fmt.Errorf("invalid provider source hostname %q in source %q: %s", hn, str, err) + } + ret.Hostname = hn + } + + return ret, nil +} diff --git a/pkg/resources/terraform/types.go b/pkg/resources/terraform/types.go new file mode 100644 index 0000000..b58149c --- /dev/null +++ b/pkg/resources/terraform/types.go @@ -0,0 +1,50 @@ +// Copyright 2024 KusionStack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package terraform + +import ( + svchost "github.com/hashicorp/terraform-svchost" + + "kusionstack.io/kusion-module-framework/pkg/resources" +) + +// Provider contains all the information of a specified Kusion provider, which not only includes the +// source address, version constraint of required provider, but also various configuration arguments used +// to configure required provider before Terraform can use them. +type Provider struct { + // Source address of the provider. + Source string `yaml:"source" json:"source"` + // Version constraint of the provider. + Version string `yaml:"version" json:"version"` + // Configuration arguments of the provider. + ProviderConfigs map[string]any `yaml:"providerConfigs" json:"providerConfigs"` +} + +// String returns a qualified string, intended for use in resource extension. +func (p Provider) String() string { + return p.Source + "/" + p.Version +} + +// TFProvider encapsulates a single terraform provider type. +type TFProvider struct { + Type string + Namespace string + Hostname svchost.Hostname +} + +// String returns an FQN string, intended for use in machine-readable output. +func (tp TFProvider) String() string { + return tp.Hostname.ForDisplay() + resources.SegmentSeparator + tp.Namespace + resources.SegmentSeparator + tp.Type +}