From 95086adabfcf9605ebe2d8569e5cc022522c3bce Mon Sep 17 00:00:00 2001 From: fullykubed Date: Thu, 12 Dec 2024 11:59:42 -0500 Subject: [PATCH] feat: metadata dastasource --- .copywrite.hcl | 2 + README.md | 9 +- docs/data-sources/metadata.md | 29 ++++++ docs/index.md | 3 + go.mod | 1 + provider/aws_tags_data_source.go | 4 +- provider/kube_labels_data_source.go | 4 +- provider/metadata_data_source.go | 147 ++++++++++++++++++++++++++++ provider/provider.go | 107 ++++++++++++++++++-- 9 files changed, 291 insertions(+), 15 deletions(-) create mode 100644 docs/data-sources/metadata.md create mode 100644 provider/metadata_data_source.go diff --git a/.copywrite.hcl b/.copywrite.hcl index b0b4173..d78d53c 100644 --- a/.copywrite.hcl +++ b/.copywrite.hcl @@ -6,6 +6,8 @@ project { copyright_year = 2024 header_ignore = [ + "go/**", + # examples used within documentation (prose) "examples/**", diff --git a/README.md b/README.md index 1a46afa..7436c53 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,11 @@ to an absolute path to the local copy of this provider repository on your local The release process is configured according to the [publishing guide provided by Hashicorp](https://developer.hashicorp.com/terraform/registry/providers/publishing) and is based on the [terraform-provider-scaffolding-framework](https://github.com/hashicorp/terraform-provider-scaffolding-framework) -repository. \ No newline at end of file +repository. + +To cut a new release: + +1. Run `go generate .` inside of the `tools` directory to update the documentation. +2. Commit your changes. +3. Tag the commit with a semver tag (e.g., `v0.0.1`). +4. Push the changes `git push --atomic origin main `. \ No newline at end of file diff --git a/docs/data-sources/metadata.md b/docs/data-sources/metadata.md new file mode 100644 index 0000000..d91682b --- /dev/null +++ b/docs/data-sources/metadata.md @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "pf_metadata Data Source - pf" +subcategory: "" +description: |- + Provides metadata about the IaC deployment context. +--- + +# pf_metadata (Data Source) + +Provides metadata about the IaC deployment context. + + + + +## Schema + +### Read-Only + +- `environment` (String) The name of the environment that you are currently deploying infrastructure to +- `is_local` (Boolean) Whether the provider is being used a part of a local development deployment +- `kube_api_server` (String) The HTTPS address of the Kubernetes API server to which infrastructure is being deployed +- `kube_cluster_name` (String) The name of the Kubernetes cluster that you are currently deploying infrastructure to +- `kube_config_context` (String) The name of the context from kubeconfig file that is being used to deploy infrastructure +- `kube_config_path` (String) The path to the kubeconfig file that is being used to deploy infrastructure +- `region` (String) The name of the region that you are currently deploying infrastructure to +- `root_module` (String) The name of the root / top-level module that you are currently deploying infrastructure with +- `stack_commit` (String) The commit hash of the Panfactum Stack that you are currently using +- `stack_version` (String) The version of the Panfactum Stack that you are currently using diff --git a/docs/index.md b/docs/index.md index 599cb3b..b1ca753 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,9 @@ description: |- - `environment` (String) The name of the environment that you are currently deploying infrastructure to - `extra_tags` (Map of String) Extra tags to apply to all resources - `is_local` (Boolean) Whether the provider is being used a part of a local development deployment +- `kube_api_server` (String) The HTTPS address of the Kubernetes API server to which infrastructure is being deployed +- `kube_cluster_name` (String) The name of the Kubernetes cluster that you are currently deploying infrastructure to +- `kube_config_context` (String) The name of the context from KUBE_CONFIG that is being used to deploy infrastructure - `region` (String) The name of the region that you are currently deploying infrastructure to - `root_module` (String) The name of the root / top-level module that you are currently deploying infrastructure with - `stack_commit` (String) The commit hash of the Panfactum Stack that you are currently using diff --git a/go.mod b/go.mod index 9abcd3b..0fde2e3 100644 --- a/go.mod +++ b/go.mod @@ -47,4 +47,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/provider/aws_tags_data_source.go b/provider/aws_tags_data_source.go index b62c922..f7f5e45 100644 --- a/provider/aws_tags_data_source.go +++ b/provider/aws_tags_data_source.go @@ -23,7 +23,7 @@ func NewAWSTagsDataSource() datasource.DataSource { } type awsTagsDataSource struct { - ProviderData *PanfactumProviderModel + ProviderData *PanfactumProvider } type awsLabelsDataSourceModel struct { @@ -67,7 +67,7 @@ func (d *awsTagsDataSource) Configure(ctx context.Context, req datasource.Config return } - data, ok := req.ProviderData.(*PanfactumProviderModel) + data, ok := req.ProviderData.(*PanfactumProvider) if !ok { resp.Diagnostics.AddError( diff --git a/provider/kube_labels_data_source.go b/provider/kube_labels_data_source.go index 8c91e94..d661a36 100644 --- a/provider/kube_labels_data_source.go +++ b/provider/kube_labels_data_source.go @@ -23,7 +23,7 @@ func NewKubeLabelsDataSource() datasource.DataSource { } type kubeLabelsDataSource struct { - ProviderData *PanfactumProviderModel + ProviderData *PanfactumProvider } type kubeLabelsDataSourceModel struct { @@ -61,7 +61,7 @@ func (d *kubeLabelsDataSource) Configure(ctx context.Context, req datasource.Con return } - data, ok := req.ProviderData.(*PanfactumProviderModel) + data, ok := req.ProviderData.(*PanfactumProvider) if !ok { resp.Diagnostics.AddError( diff --git a/provider/metadata_data_source.go b/provider/metadata_data_source.go new file mode 100644 index 0000000..df0ec5d --- /dev/null +++ b/provider/metadata_data_source.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +/************************************************************** + Provider Definition + **************************************************************/ + +var _ datasource.DataSource = &metadataDataSource{} + +func NewMetadataDataSource() datasource.DataSource { + return &metadataDataSource{} +} + +type metadataDataSource struct { + ProviderData *PanfactumProvider +} + +type metadataDataSourceModel struct { + Environment types.String `tfsdk:"environment"` + Region types.String `tfsdk:"region"` + RootModule types.String `tfsdk:"root_module"` + StackVersion types.String `tfsdk:"stack_version"` + StackCommit types.String `tfsdk:"stack_commit"` + IsLocal types.Bool `tfsdk:"is_local"` + KubeConfigPath types.String `tfsdk:"kube_config_path"` + KubeConfigContext types.String `tfsdk:"kube_config_context"` + KubeAPIServer types.String `tfsdk:"kube_api_server"` + KubeClusterName types.String `tfsdk:"kube_cluster_name"` +} + +func (d *metadataDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_metadata" +} + +func (d *metadataDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Provides metadata about the IaC deployment context.", + MarkdownDescription: "Provides metadata about the IaC deployment context.", + + Attributes: map[string]schema.Attribute{ + "environment": schema.StringAttribute{ + Description: "The name of the environment that you are currently deploying infrastructure to", + MarkdownDescription: "The name of the environment that you are currently deploying infrastructure to", + Computed: true, + }, + "region": schema.StringAttribute{ + Description: "The name of the region that you are currently deploying infrastructure to", + MarkdownDescription: "The name of the region that you are currently deploying infrastructure to", + Computed: true, + }, + "root_module": schema.StringAttribute{ + Computed: true, + Description: "The name of the root / top-level module that you are currently deploying infrastructure with", + MarkdownDescription: "The name of the root / top-level module that you are currently deploying infrastructure with", + }, + "stack_version": schema.StringAttribute{ + Computed: true, + Description: "The version of the Panfactum Stack that you are currently using", + MarkdownDescription: "The version of the Panfactum Stack that you are currently using", + }, + "stack_commit": schema.StringAttribute{ + Computed: true, + Description: "The commit hash of the Panfactum Stack that you are currently using", + MarkdownDescription: "The commit hash of the Panfactum Stack that you are currently using", + }, + "is_local": schema.BoolAttribute{ + Computed: true, + Description: "Whether the provider is being used a part of a local development deployment", + MarkdownDescription: "Whether the provider is being used a part of a local development deployment", + }, + "kube_config_path": schema.StringAttribute{ + Description: "The path to the kubeconfig file that is being used to deploy infrastructure", + MarkdownDescription: "The path to the kubeconfig file that is being used to deploy infrastructure", + Computed: true, + }, + "kube_config_context": schema.StringAttribute{ + Description: "The name of the context from the kubeconfig file that is being used to deploy infrastructure", + MarkdownDescription: "The name of the context from kubeconfig file that is being used to deploy infrastructure", + Computed: true, + }, + "kube_api_server": schema.StringAttribute{ + Description: "The HTTPS address of the Kubernetes API server to which infrastructure is being deployed", + MarkdownDescription: "The HTTPS address of the Kubernetes API server to which infrastructure is being deployed", + Computed: true, + }, + "kube_cluster_name": schema.StringAttribute{ + Description: "The name of the Kubernetes cluster that you are currently deploying infrastructure to", + MarkdownDescription: "The name of the Kubernetes cluster that you are currently deploying infrastructure to", + Computed: true, + }, + }, + } +} + +func (d *metadataDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*PanfactumProvider) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected PanfactumProviderModel, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.ProviderData = data +} + +func (d *metadataDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + + var data metadataDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + data.Environment = d.ProviderData.Environment + data.Region = d.ProviderData.Region + data.RootModule = d.ProviderData.RootModule + data.StackCommit = d.ProviderData.StackCommit + data.StackVersion = d.ProviderData.StackVersion + data.IsLocal = d.ProviderData.IsLocal + data.KubeConfigPath = types.StringValue(d.ProviderData.KubeConfigPath) + data.KubeConfigContext = d.ProviderData.KubeConfigContext + data.KubeAPIServer = d.ProviderData.KubeAPIServer + data.KubeClusterName = d.ProviderData.KubeClusterName + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/provider/provider.go b/provider/provider.go index 039bc2f..61dc5cc 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -5,25 +5,34 @@ package provider import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" + "gopkg.in/yaml.v3" + "os" + "path/filepath" ) type PanfactumProvider struct { + *PanfactumProviderModel + KubeConfigPath string } type PanfactumProviderModel struct { - Environment types.String `tfsdk:"environment"` - Region types.String `tfsdk:"region"` - RootModule types.String `tfsdk:"root_module"` - StackVersion types.String `tfsdk:"stack_version"` - StackCommit types.String `tfsdk:"stack_commit"` - IsLocal types.Bool `tfsdk:"is_local"` - ExtraTags types.Map `tfsdk:"extra_tags"` + Environment types.String `tfsdk:"environment"` + Region types.String `tfsdk:"region"` + RootModule types.String `tfsdk:"root_module"` + StackVersion types.String `tfsdk:"stack_version"` + StackCommit types.String `tfsdk:"stack_commit"` + IsLocal types.Bool `tfsdk:"is_local"` + ExtraTags types.Map `tfsdk:"extra_tags"` + KubeConfigContext types.String `tfsdk:"kube_config_context"` + KubeAPIServer types.String `tfsdk:"kube_api_server"` + KubeClusterName types.String `tfsdk:"kube_cluster_name"` } func New() provider.Provider { @@ -73,14 +82,56 @@ func (p *PanfactumProvider) Schema(ctx context.Context, req provider.SchemaReque MarkdownDescription: "Extra tags to apply to all resources", ElementType: types.StringType, }, + "kube_config_context": schema.StringAttribute{ + Description: "The name of the context from KUBE_CONFIG that is being used to deploy infrastructure", + MarkdownDescription: "The name of the context from KUBE_CONFIG that is being used to deploy infrastructure", + Optional: true, + }, + "kube_api_server": schema.StringAttribute{ + Description: "The HTTPS address of the Kubernetes API server to which infrastructure is being deployed", + MarkdownDescription: "The HTTPS address of the Kubernetes API server to which infrastructure is being deployed", + Optional: true, + }, + "kube_cluster_name": schema.StringAttribute{ + Description: "The name of the Kubernetes cluster that you are currently deploying infrastructure to", + MarkdownDescription: "The name of the Kubernetes cluster that you are currently deploying infrastructure to", + Optional: true, + }, }, } } func (p *PanfactumProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { - var data PanfactumProviderModel - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - resp.DataSourceData = &data + var model PanfactumProviderModel + var newProvider = PanfactumProvider{PanfactumProviderModel: &model} + + // Step 1: Load the explicitly set data + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + + // Step 2: Load config from environment variables + kubeCfgPath := os.Getenv("KUBE_CONFIG_PATH") + if kubeCfgPath != "" { + newProvider.KubeConfigPath = filepath.Clean(kubeCfgPath) + } else { + homePath, err := os.UserHomeDir() + if err != nil { + resp.Diagnostics.AddError("Unable to load user home directory", fmt.Sprintf("%v", err)) + } + newProvider.KubeConfigPath = filepath.Join(homePath, ".kube/config") + } + + // Step 3: Load the cluster name based on the current context + kubeCfgContext := newProvider.KubeConfigContext.ValueString() + if kubeCfgContext != "" { + if clusterName, err := getKubeClusterName(newProvider.KubeConfigPath, kubeCfgContext); err != nil { + resp.Diagnostics.AddError("Unable to load cluster name", fmt.Sprintf("%v", err)) + } else { + newProvider.KubeClusterName = types.StringValue(clusterName) + } + } + + resp.DataSourceData = &newProvider + resp.ResourceData = &newProvider } func (p *PanfactumProvider) Resources(ctx context.Context) []func() resource.Resource { @@ -91,6 +142,7 @@ func (p *PanfactumProvider) DataSources(ctx context.Context) []func() datasource return []func() datasource.DataSource{ NewKubeLabelsDataSource, NewAWSTagsDataSource, + NewMetadataDataSource, } } @@ -100,3 +152,38 @@ func (p *PanfactumProvider) Functions(ctx context.Context) []func() function.Fun NewSanitizeKubeLabelsFunction, } } + +/************************************************************** + Utility Functions + **************************************************************/ + +type KubeConfig struct { + Contexts []struct { + Name string `yaml:"name"` + Context struct { + Cluster string `yaml:"cluster"` + } `yaml:"context"` + } `yaml:"contexts"` +} + +func getKubeClusterName(kubeConfigPath string, context string) (string, error) { + file, err := os.Open(kubeConfigPath) + if err != nil { + return "", fmt.Errorf("error opening YAML file: %v", err) + } + defer file.Close() + + var cfg KubeConfig + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(&cfg); err != nil { + return "", fmt.Errorf("error decoding YAML: %v", err) + } + + for _, contextConfig := range cfg.Contexts { + if contextConfig.Name == context { + return contextConfig.Context.Cluster, nil + } + } + + return "", fmt.Errorf("no context name %s found in kubeconfig file at %s", context, kubeConfigPath) +}