diff --git a/client/client.go b/client/client.go index fb657ca..aea3f89 100644 --- a/client/client.go +++ b/client/client.go @@ -45,6 +45,7 @@ type endpoints struct { LandingZones *url.URL `json:"meshlandingzones"` Platforms *url.URL `json:"meshplatforms"` PaymentMethods *url.URL `json:"meshpaymentmethods"` + Integrations *url.URL `json:"meshintegrations"` } type loginRequest struct { @@ -82,6 +83,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro LandingZones: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlandingzones"), Platforms: rootUrl.JoinPath(apiMeshObjectsRoot, "meshplatforms"), PaymentMethods: rootUrl.JoinPath(apiMeshObjectsRoot, "meshpaymentmethods"), + Integrations: rootUrl.JoinPath(apiMeshObjectsRoot, "meshintegrations"), } return client, nil @@ -111,7 +113,7 @@ func (c *MeshStackProviderClient) login() error { if err != nil { return err } else if res.StatusCode != 200 { - return errors.New(fmt.Sprintf("Status %d: %s", res.StatusCode, ERROR_AUTHENTICATION_FAILURE)) + return fmt.Errorf("Status %d: %s", res.StatusCode, ERROR_AUTHENTICATION_FAILURE) } defer res.Body.Close() diff --git a/client/integrations.go b/client/integrations.go new file mode 100644 index 0000000..0b686f5 --- /dev/null +++ b/client/integrations.go @@ -0,0 +1,195 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_INTEGRATION = "application/vnd.meshcloud.api.meshintegration.v1-preview.hal+json" + +type MeshIntegration struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshIntegrationMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshIntegrationSpec `json:"spec" tfsdk:"spec"` + Status *MeshIntegrationStatus `json:"status,omitempty" tfsdk:"status"` +} + +type MeshIntegrationMetadata struct { + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn *string `json:"createdOn,omitempty" tfsdk:"created_on"` +} + +type MeshIntegrationSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Config MeshIntegrationConfig `json:"config" tfsdk:"config"` +} + +type MeshIntegrationStatus struct { + IsBuiltIn bool `json:"isBuiltIn" tfsdk:"is_built_in"` + WorkloadIdentityFederation *MeshWorkloadIdentityFederation `json:"workloadIdentityFederation,omitempty" tfsdk:"workload_identity_federation"` +} + +// Integration Config wrapper with type discrimination +type MeshIntegrationConfig struct { + Type string `json:"type" tfsdk:"type"` + Github *MeshIntegrationGithubConfig `json:"github,omitempty" tfsdk:"github"` + Gitlab *MeshIntegrationGitlabConfig `json:"gitlab,omitempty" tfsdk:"gitlab"` + AzureDevops *MeshIntegrationAzureDevopsConfig `json:"azuredevops,omitempty" tfsdk:"azuredevops"` +} + +// GitHub Integration +type MeshIntegrationGithubConfig struct { + Owner string `json:"owner" tfsdk:"owner"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + AppId string `json:"appId" tfsdk:"app_id"` + AppPrivateKey string `json:"appPrivateKey" tfsdk:"app_private_key"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +// GitLab Integration +type MeshIntegrationGitlabConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +// Azure DevOps Integration +type MeshIntegrationAzureDevopsConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + Organization string `json:"organization" tfsdk:"organization"` + PersonalAccessToken string `json:"personalAccessToken" tfsdk:"personal_access_token"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +// Building Block Runner Reference +type BuildingBlockRunnerRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind" tfsdk:"kind"` +} + +// Workload Identity Federation +type MeshWorkloadIdentityFederation struct { + Issuer string `json:"issuer" tfsdk:"issuer"` + Subject string `json:"subject" tfsdk:"subject"` + Gcp *MeshWifProvider `json:"gcp,omitempty" tfsdk:"gcp"` + Aws *MeshAwsWifProvider `json:"aws,omitempty" tfsdk:"aws"` + Azure *MeshWifProvider `json:"azure,omitempty" tfsdk:"azure"` +} + +type MeshWifProvider struct { + Audience string `json:"audience" tfsdk:"audience"` +} + +type MeshAwsWifProvider struct { + Audience string `json:"audience" tfsdk:"audience"` + Thumbprint string `json:"thumbprint" tfsdk:"thumbprint"` +} + +func (c *MeshStackProviderClient) urlForIntegration(workspace string, uuid string) *url.URL { + return c.endpoints.Integrations.JoinPath(workspace, uuid) +} + +func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) (*MeshIntegration, error) { + targetUrl := c.urlForIntegration(workspace, uuid) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if res.StatusCode == http.StatusNotFound { + return nil, nil + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var integration MeshIntegration + err = json.Unmarshal(data, &integration) + if err != nil { + return nil, err + } + + return &integration, nil +} + +func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) { + var allIntegrations []MeshIntegration + + pageNumber := 0 + targetUrl := c.endpoints.Integrations + query := targetUrl.Query() + + for { + query.Set("page", fmt.Sprintf("%d", pageNumber)) + targetUrl.RawQuery = query.Encode() + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var response struct { + Embedded struct { + MeshIntegrations []MeshIntegration `json:"meshIntegrations"` + } `json:"_embedded"` + Page struct { + Size int `json:"size"` + TotalElements int `json:"totalElements"` + TotalPages int `json:"totalPages"` + Number int `json:"number"` + } `json:"page"` + } + + err = json.Unmarshal(data, &response) + if err != nil { + return nil, err + } + + allIntegrations = append(allIntegrations, response.Embedded.MeshIntegrations...) + + // Check if there are more pages + if response.Page.Number >= response.Page.TotalPages-1 { + break + } + + pageNumber++ + } + + return &allIntegrations, nil +} diff --git a/internal/provider/integrations_data_source.go b/internal/provider/integrations_data_source.go new file mode 100644 index 0000000..562d7de --- /dev/null +++ b/internal/provider/integrations_data_source.go @@ -0,0 +1,260 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/meshcloud/terraform-provider-meshstack/client" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &integrationsDataSource{} +) + +// NewIntegrationsDataSource is a helper function to simplify the provider implementation. +func NewIntegrationsDataSource() datasource.DataSource { + return &integrationsDataSource{} +} + +// integrationsDataSource is the data source implementation. +type integrationsDataSource struct { + client *client.MeshStackProviderClient +} + +// Metadata returns the data source type name. +func (d *integrationsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_integrations" +} + +// Schema defines the schema for the data source. +func (d *integrationsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + workloadIdentityFederation := schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "issuer": schema.StringAttribute{ + Computed: true, + }, + "subject": schema.StringAttribute{ + Computed: true, + }, + "gcp": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + Computed: true, + }, + }, + }, + "aws": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + Computed: true, + }, + "thumbprint": schema.StringAttribute{ + Computed: true, + }, + }, + }, + "azure": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "audience": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + } + + resp.Schema = schema.Schema{ + MarkdownDescription: "List of integrations.", + + Attributes: map[string]schema.Attribute{ + "workload_identity_federation": schema.SingleNestedAttribute{ + MarkdownDescription: "Workload identity federation information for built in integrations.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "replicator": workloadIdentityFederation, + "metering": workloadIdentityFederation, + }, + }, + "integrations": schema.ListNestedAttribute{ + MarkdownDescription: "List of integrations", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + Computed: true, + }, + "kind": schema.StringAttribute{ + Computed: true, + }, + "metadata": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Computed: true, + }, + "owned_by_workspace": schema.StringAttribute{ + Computed: true, + }, + "created_on": schema.StringAttribute{ + Computed: true, + }, + }, + }, + "spec": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + Computed: true, + }, + "config": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Computed: true, + }, + "github": schema.SingleNestedAttribute{ + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "owner": schema.StringAttribute{ + Computed: true, + }, + "base_url": schema.StringAttribute{ + Computed: true, + }, + "app_id": schema.StringAttribute{ + Computed: true, + }, + "app_private_key": schema.StringAttribute{ + Computed: true, + }, + "runner_ref": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Computed: true, + }, + "kind": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + "gitlab": schema.SingleNestedAttribute{ + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "base_url": schema.StringAttribute{ + Computed: true, + }, + "runner_ref": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Computed: true, + }, + "kind": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + "azuredevops": schema.SingleNestedAttribute{ + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "base_url": schema.StringAttribute{ + Computed: true, + }, + "organization": schema.StringAttribute{ + Computed: true, + }, + "personal_access_token": schema.StringAttribute{ + Computed: true, + }, + "runner_ref": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Computed: true, + }, + "kind": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "status": schema.SingleNestedAttribute{ + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "is_built_in": schema.BoolAttribute{ + Computed: true, + }, + "workload_identity_federation": workloadIdentityFederation, + }, + }, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the data source. +func (d *integrationsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.MeshStackProviderClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + d.client = client +} + +// Read refreshes the Terraform state with the latest data. +func (d *integrationsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + integrations, err := d.client.ReadIntegrations() + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read integrations, got error: %s", err)) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("integrations"), &integrations)...) + + for _, integration := range *integrations { + if integration.Status != nil && integration.Status.IsBuiltIn { + if integration.Spec.Config.Type == "replicator" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, + path.Root("workload_identity_federation").AtName("replicator"), integration.Status.WorkloadIdentityFederation)...) + } + if integration.Spec.Config.Type == "metering" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, + path.Root("workload_identity_federation").AtName("metering"), integration.Status.WorkloadIdentityFederation)...) + } + } + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index da9473c..4b5a919 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -145,6 +145,7 @@ func (p *MeshStackProvider) DataSources(ctx context.Context) []func() datasource NewLandingZoneDataSource, NewPlatformDataSource, NewPaymentMethodDataSource, + NewIntegrationsDataSource, } }