diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 826641a..d8eb5e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,8 @@ on: paths-ignore: - "README.md" push: + branches: + - main paths-ignore: - "README.md" diff --git a/docs/data-sources/contract.md b/docs/data-sources/contract.md index 7f1921c..8dc01f1 100644 --- a/docs/data-sources/contract.md +++ b/docs/data-sources/contract.md @@ -31,7 +31,10 @@ data "cosmo_subgraph" "test" { - `admission_webhook_secret` (String, Sensitive) The secret token used to authenticate the admission webhook requests. - `admission_webhook_url` (String) The URL for the admission webhook that will be triggered during graph operations. +- `exclude_tags` (List of String) - `id` (String) The unique identifier of the federated graph resource, automatically generated by the system. +- `include_tags` (List of String) - `label_matchers` (Map of String) A list of label matchers used to select the services that will form the federated graph. - `readme` (String) Readme content for the federated graph. - `routing_url` (String) The URL for the federated graph. +- `supports_federation` (Boolean) SupportFederation defines if this contract's source graph is a federated graph or a monograph.. diff --git a/docs/resources/contract.md b/docs/resources/contract.md index 47e1e39..ce80160 100644 --- a/docs/resources/contract.md +++ b/docs/resources/contract.md @@ -40,7 +40,9 @@ resource "cosmo_contract" "test" { - `admission_webhook_secret` (String) - `admission_webhook_url` (String) - `exclude_tags` (List of String) +- `include_tags` (List of String) - `readme` (String) +- `supports_federation` (Boolean) ### Read-Only diff --git a/docs/resources/monograph.md b/docs/resources/monograph.md index 28101a4..f48c40b 100644 --- a/docs/resources/monograph.md +++ b/docs/resources/monograph.md @@ -21,6 +21,7 @@ resource "cosmo_monograph" "example" { namespace = var.monograph_namespace graph_url = var.monograph_graph_url routing_url = var.monograph_routing_url + schema = var.monograph_schema } ``` @@ -39,6 +40,7 @@ resource "cosmo_monograph" "example" { - `admission_webhook_url` (String) The admission webhook URL for the monograph. - `namespace` (String) The namespace in which the monograph is located. - `readme` (String) The readme for the subgraph. +- `schema` (String) The schema for the subgraph. - `subscription_protocol` (String) The subscription protocol for the subgraph. - `subscription_url` (String) The subscription URL for the subgraph. - `websocket_subprotocol` (String) The websocket subprotocol for the subgraph. diff --git a/examples/guides/cosmo-monograph-contract/main.tf b/examples/guides/cosmo-monograph-contract/main.tf index 169320e..303f1f4 100644 --- a/examples/guides/cosmo-monograph-contract/main.tf +++ b/examples/guides/cosmo-monograph-contract/main.tf @@ -11,6 +11,7 @@ module "cosmo_monograph" { monograph_namespace = module.cosmo_namespace.name monograph_graph_url = var.monograph_graph_url monograph_routing_url = var.monograph_routing_url + monograph_schema = var.monograph_schema } module "cosmo_contract" { diff --git a/examples/guides/cosmo-monograph-contract/variables.tf b/examples/guides/cosmo-monograph-contract/variables.tf index b5e2980..953a14f 100644 --- a/examples/guides/cosmo-monograph-contract/variables.tf +++ b/examples/guides/cosmo-monograph-contract/variables.tf @@ -14,6 +14,10 @@ variable "monograph_routing_url" { default = "http://example.com/routing" } +variable "monograph_schema" { + default = "type Query{ a: String }" +} + variable "contract_name" { type = string default = "test" diff --git a/examples/resources/cosmo_monograph/resource.tf b/examples/resources/cosmo_monograph/resource.tf index 5ca4473..1a00bad 100644 --- a/examples/resources/cosmo_monograph/resource.tf +++ b/examples/resources/cosmo_monograph/resource.tf @@ -3,4 +3,5 @@ resource "cosmo_monograph" "example" { namespace = var.monograph_namespace graph_url = var.monograph_graph_url routing_url = var.monograph_routing_url + schema = var.monograph_schema } \ No newline at end of file diff --git a/examples/resources/cosmo_monograph/variables.tf b/examples/resources/cosmo_monograph/variables.tf index 60ed5e8..a9afb35 100644 --- a/examples/resources/cosmo_monograph/variables.tf +++ b/examples/resources/cosmo_monograph/variables.tf @@ -12,4 +12,8 @@ variable "monograph_graph_url" { variable "monograph_routing_url" { default = "http://example.com/routing" +} + +variable "monograph_schema" { + default = "type Query{ a: String }" } \ No newline at end of file diff --git a/go.mod b/go.mod index a6d8d9d..ed6542f 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.10.0 - github.com/wundergraph/cosmo/connect-go v0.0.0-20240916094337-a4c4cae55557 + github.com/wundergraph/cosmo/connect-go v0.0.0-20241203152720-979e5a780c8e ) require ( diff --git a/go.sum b/go.sum index 37819ab..9df6555 100644 --- a/go.sum +++ b/go.sum @@ -196,8 +196,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wundergraph/cosmo/connect-go v0.0.0-20240916094337-a4c4cae55557 h1:hzZKQsFVJZ6JqOlTWT73P7GNY2vkDV3J+3oYX0SN/iY= -github.com/wundergraph/cosmo/connect-go v0.0.0-20240916094337-a4c4cae55557/go.mod h1:RLepGeaXdENMlePq4geGzaDV895QuJZ+mUFLzG5ra9E= +github.com/wundergraph/cosmo/connect-go v0.0.0-20241203152720-979e5a780c8e h1:XFEGOGnBWR6cfW3gV+24An3lNoesWOxTH71D09Tuph4= +github.com/wundergraph/cosmo/connect-go v0.0.0-20241203152720-979e5a780c8e/go.mod h1:RLepGeaXdENMlePq4geGzaDV895QuJZ+mUFLzG5ra9E= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/internal/api/common.go b/internal/api/common.go index 2efa0c0..7c1ab0f 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -10,7 +10,7 @@ const ( GraphQLWebsocketSubprotocolGraphQLTransportWS = "graphql-transport-ws" ) -func resolveWebsocketSubprotocol(protocol string) *common.GraphQLWebsocketSubprotocol { +func ResolveWebsocketSubprotocol(protocol string) *common.GraphQLWebsocketSubprotocol { switch protocol { case GraphQLWebsocketSubprotocolGraphQLWS: return common.GraphQLWebsocketSubprotocol_GRAPHQL_WEBSOCKET_SUBPROTOCOL_WS.Enum() @@ -28,7 +28,7 @@ const ( GraphQLSubscriptionProtocolSSEPost = "sse_post" ) -func resolveSubscriptionProtocol(protocol string) *common.GraphQLSubscriptionProtocol { +func ResolveSubscriptionProtocol(protocol string) *common.GraphQLSubscriptionProtocol { switch protocol { case GraphQLSubscriptionProtocolSSE: return common.GraphQLSubscriptionProtocol_GRAPHQL_SUBSCRIPTION_PROTOCOL_SSE.Enum() diff --git a/internal/api/contract.go b/internal/api/contract.go index 75c172a..acd0e47 100644 --- a/internal/api/contract.go +++ b/internal/api/contract.go @@ -8,17 +8,8 @@ import ( platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" ) -func (p *PlatformClient) CreateContract(ctx context.Context, name, namespace, sourceGraphName, routingUrl, admissionWebhookUrl, admissionWebhookSecret string, excludeTags []string, readme string) (*platformv1.CreateContractResponse, *ApiError) { - request := connect.NewRequest(&platformv1.CreateContractRequest{ - Name: name, - Namespace: namespace, - SourceGraphName: sourceGraphName, - RoutingUrl: routingUrl, - AdmissionWebhookUrl: admissionWebhookUrl, - ExcludeTags: excludeTags, - Readme: &readme, - AdmissionWebhookSecret: &admissionWebhookSecret, - }) +func (p *PlatformClient) CreateContract(ctx context.Context, data *platformv1.CreateContractRequest) (*platformv1.CreateContractResponse, *ApiError) { + request := connect.NewRequest(data) response, err := p.Client.CreateContract(ctx, request) if err != nil { @@ -37,12 +28,8 @@ func (p *PlatformClient) CreateContract(ctx context.Context, name, namespace, so return response.Msg, nil } -func (p *PlatformClient) UpdateContract(ctx context.Context, name, namespace string, excludeTags []string) (*platformv1.UpdateContractResponse, *ApiError) { - request := connect.NewRequest(&platformv1.UpdateContractRequest{ - Name: name, - Namespace: namespace, - ExcludeTags: excludeTags, - }) +func (p *PlatformClient) UpdateContract(ctx context.Context, data *platformv1.UpdateContractRequest) (*platformv1.UpdateContractResponse, *ApiError) { + request := connect.NewRequest(data) response, err := p.Client.UpdateContract(ctx, request) if err != nil { @@ -61,8 +48,12 @@ func (p *PlatformClient) UpdateContract(ctx context.Context, name, namespace str return response.Msg, nil } -func (p *PlatformClient) DeleteContract(ctx context.Context, name, namespace string) *ApiError { - return p.DeleteFederatedGraph(ctx, name, namespace) +func (p *PlatformClient) DeleteContract(ctx context.Context, name, namespace string, supportsFederation bool) *ApiError { + if supportsFederation { + return p.DeleteFederatedGraph(ctx, name, namespace) + } else { + return p.DeleteMonograph(ctx, name, namespace) + } } func (p *PlatformClient) GetContract(ctx context.Context, name, namespace string) (*platformv1.GetFederatedGraphByNameResponse, *ApiError) { diff --git a/internal/api/monograph.go b/internal/api/monograph.go index ea5d090..0756e6a 100644 --- a/internal/api/monograph.go +++ b/internal/api/monograph.go @@ -16,8 +16,8 @@ func (p PlatformClient) CreateMonograph(ctx context.Context, name string, namesp GraphUrl: graphUrl, SubscriptionUrl: subscriptionUrl, Readme: readme, - WebsocketSubprotocol: resolveWebsocketSubprotocol(websocketSubprotocol), - SubscriptionProtocol: resolveSubscriptionProtocol(subscriptionProtocol), + WebsocketSubprotocol: ResolveWebsocketSubprotocol(websocketSubprotocol), + SubscriptionProtocol: ResolveSubscriptionProtocol(subscriptionProtocol), AdmissionWebhookURL: admissionWebhookUrl, AdmissionWebhookSecret: &admissionWebhookSecret, }) @@ -46,8 +46,8 @@ func (p PlatformClient) UpdateMonograph(ctx context.Context, name string, namesp GraphUrl: graphUrl, SubscriptionUrl: subscriptionUrl, Readme: readme, - WebsocketSubprotocol: resolveWebsocketSubprotocol(websocketSubprotocol), - SubscriptionProtocol: resolveSubscriptionProtocol(subscriptionProtocol), + WebsocketSubprotocol: ResolveWebsocketSubprotocol(websocketSubprotocol), + SubscriptionProtocol: ResolveSubscriptionProtocol(subscriptionProtocol), AdmissionWebhookURL: &admissionWebhookUrl, AdmissionWebhookSecret: &admissionWebhookSecret, }) @@ -111,3 +111,26 @@ func (p PlatformClient) GetMonograph(ctx context.Context, name string, namespace return response.Msg.Graph, nil } + +func (p PlatformClient) PublishMonograph(ctx context.Context, name string, namespace string, schema string) *ApiError { + request := connect.NewRequest(&platformv1.PublishMonographRequest{ + Name: name, + Namespace: namespace, + Schema: schema, + }) + response, err := p.Client.PublishMonograph(ctx, request) + if err != nil { + return &ApiError{Err: err, Reason: "PublishMonograph", Status: common.EnumStatusCode_ERR} + } + + if response.Msg == nil { + return &ApiError{Err: ErrEmptyMsg, Reason: "PublishMonograph", Status: common.EnumStatusCode_ERR} + } + + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError + } + + return nil +} diff --git a/internal/api/subgraph.go b/internal/api/subgraph.go index a0712ff..54454c0 100644 --- a/internal/api/subgraph.go +++ b/internal/api/subgraph.go @@ -8,20 +8,8 @@ import ( platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" ) -func (p PlatformClient) CreateSubgraph(ctx context.Context, name string, namespace string, routingUrl string, baseSubgraphName *string, labels []*platformv1.Label, subscriptionUrl *string, readme *string, isEventDrivenGraph *bool, isFeatureSubgraph *bool, subscriptionProtocol string, websocketSubprotocol string) *ApiError { - request := connect.NewRequest(&platformv1.CreateFederatedSubgraphRequest{ - Name: name, - Namespace: namespace, - RoutingUrl: &routingUrl, - Labels: labels, - SubscriptionUrl: subscriptionUrl, - Readme: readme, - WebsocketSubprotocol: resolveWebsocketSubprotocol(websocketSubprotocol), - SubscriptionProtocol: resolveSubscriptionProtocol(subscriptionProtocol), - IsEventDrivenGraph: isEventDrivenGraph, - BaseSubgraphName: baseSubgraphName, - IsFeatureSubgraph: isFeatureSubgraph, - }) +func (p PlatformClient) CreateSubgraph(ctx context.Context, data *platformv1.CreateFederatedSubgraphRequest) *ApiError { + request := connect.NewRequest(data) response, err := p.Client.CreateFederatedSubgraph(ctx, request) if err != nil { return &ApiError{Err: err, Reason: "CreateSubgraph", Status: common.EnumStatusCode_ERR} @@ -39,19 +27,8 @@ func (p PlatformClient) CreateSubgraph(ctx context.Context, name string, namespa return nil } -func (p PlatformClient) UpdateSubgraph(ctx context.Context, name, namespace, routingUrl string, labels []*platformv1.Label, headers []string, subscriptionUrl, readme *string, unsetLabels *bool, websocketSubprotocol string, subscriptionProtocol string) *ApiError { - request := connect.NewRequest(&platformv1.UpdateSubgraphRequest{ - Name: name, - RoutingUrl: &routingUrl, - Labels: labels, - Headers: headers, - SubscriptionUrl: subscriptionUrl, - Readme: readme, - Namespace: namespace, - UnsetLabels: unsetLabels, - WebsocketSubprotocol: resolveWebsocketSubprotocol(websocketSubprotocol), - SubscriptionProtocol: resolveSubscriptionProtocol(subscriptionProtocol), - }) +func (p PlatformClient) UpdateSubgraph(ctx context.Context, data *platformv1.UpdateSubgraphRequest) *ApiError { + request := connect.NewRequest(data) response, err := p.Client.UpdateSubgraph(ctx, request) if err != nil { diff --git a/internal/service/contract/data_source_cosmo_contract.go b/internal/service/contract/data_source_cosmo_contract.go index 42ef513..b29b6f2 100644 --- a/internal/service/contract/data_source_cosmo_contract.go +++ b/internal/service/contract/data_source_cosmo_contract.go @@ -29,6 +29,9 @@ type contractDataSourceModel struct { AdmissionWebhookUrl types.String `tfsdk:"admission_webhook_url"` AdmissionWebhookSecret types.String `tfsdk:"admission_webhook_secret"` LabelMatchers types.Map `tfsdk:"label_matchers"` + ExcludeTags types.List `tfsdk:"exclude_tags"` + IncludeTags types.List `tfsdk:"include_tags"` + SupportsFederation types.Bool `tfsdk:"supports_federation"` } func (d *contractDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -72,6 +75,18 @@ func (d *contractDataSource) Schema(ctx context.Context, req datasource.SchemaRe MarkdownDescription: "The URL for the federated graph.", Computed: true, }, + "exclude_tags": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + }, + "include_tags": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + }, + "supports_federation": schema.BoolAttribute{ + MarkdownDescription: "SupportFederation defines if this contract's source graph is a federated graph or a monograph..", + Computed: true, + }, }, } } @@ -124,22 +139,36 @@ func (d *contractDataSource) Read(ctx context.Context, req datasource.ReadReques } graph := apiResponse.Graph - data.Id = types.StringValue(graph.GetId()) data.Name = types.StringValue(graph.GetName()) data.Namespace = types.StringValue(graph.GetNamespace()) data.RoutingURL = types.StringValue(graph.GetRoutingURL()) + data.SupportsFederation = types.BoolValue(graph.GetSupportsFederation()) + + if graph.Contract != nil && len(graph.Contract.GetExcludeTags()) > 0 { + var responseExcludeTags []attr.Value + for _, tag := range graph.Contract.GetExcludeTags() { + responseExcludeTags = append(responseExcludeTags, types.StringValue(tag)) + } + data.ExcludeTags = types.ListValueMust(types.StringType, responseExcludeTags) + } - labelMatchers := make(map[string]attr.Value) - for _, labelMatcher := range graph.GetLabelMatchers() { - labelMatchers[labelMatcher] = types.StringValue(labelMatcher) + if graph.Contract != nil && len(graph.Contract.GetIncludeTags()) > 0 { + var responseIncludeTags []attr.Value + for _, tag := range graph.Contract.IncludeTags { + responseIncludeTags = append(responseIncludeTags, types.StringValue(tag)) + } + data.IncludeTags = types.ListValueMust(types.StringType, responseIncludeTags) } - data.LabelMatchers = types.MapValueMust(types.StringType, labelMatchers) if graph.Readme != nil { data.Readme = types.StringValue(*graph.Readme) } + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } + tflog.Trace(ctx, "Read contract data source", map[string]interface{}{ "id": data.Id.ValueString(), }) diff --git a/internal/service/contract/data_source_cosmo_contract_test.go b/internal/service/contract/data_source_cosmo_contract_test.go index e8dacb3..49ff792 100644 --- a/internal/service/contract/data_source_cosmo_contract_test.go +++ b/internal/service/contract/data_source_cosmo_contract_test.go @@ -13,12 +13,16 @@ func TestAccContractDataSource(t *testing.T) { name := acctest.RandomWithPrefix("test-contract") namespace := acctest.RandomWithPrefix("test-namespace") + subgraphName := acctest.RandomWithPrefix("test-subgraph") + subgraphRoutingURL := "https://subgraph-standalone-example.com" + subgraphSchema := acceptance.TestAccValidSubgraphSchema + resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccContractDataSourceConfig(namespace, name), + Config: testAccContractDataSourceConfig(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, name), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.cosmo_contract.test", "name", name), resource.TestCheckResourceAttr("data.cosmo_contract.test", "namespace", namespace), @@ -28,27 +32,41 @@ func TestAccContractDataSource(t *testing.T) { ResourceName: "data.cosmo_contract.test", RefreshState: true, }, + { + Config: testAccContractDataSourceConfig(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, name), + Destroy: true, + }, }, }) } -func testAccContractDataSourceConfig(namespace, name string) string { +func testAccContractDataSourceConfig(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, name string) string { return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" } -resource "cosmo_monograph" "source_graph" { +resource "cosmo_federated_graph" "source_graph" { name = "source-graph" namespace = cosmo_namespace.test.name routing_url = "https://example.com" - graph_url = "https://example.com" + depends_on = [cosmo_subgraph.test] +} + +resource "cosmo_subgraph" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + routing_url = "%s" + schema = <<-EOT + %s + EOT + labels = {} } resource "cosmo_contract" "test" { name = "%s" namespace = cosmo_namespace.test.name - source = cosmo_monograph.source_graph.name + source = cosmo_federated_graph.source_graph.name routing_url = "https://example.com" readme = "Initial readme content" } @@ -57,5 +75,5 @@ data "cosmo_contract" "test" { name = cosmo_contract.test.name namespace = cosmo_contract.test.namespace } -`, namespace, name) +`, namespace, subgraphName, subgraphRoutingURL, subgraphSchema, name) } diff --git a/internal/service/contract/errors.go b/internal/service/contract/errors.go index d668338..8c54162 100644 --- a/internal/service/contract/errors.go +++ b/internal/service/contract/errors.go @@ -12,3 +12,7 @@ const ( ErrUnexpectedDataSourceType = "Unexpected Data Source Configure Type" ErrUnexpectedResourceType = "Unexpected Resource Configure Type" ) + +const ( + DebugCreate = "create-contract" +) diff --git a/internal/service/contract/resource_cosmo_contract.go b/internal/service/contract/resource_cosmo_contract.go index d55d1ad..af1e83c 100644 --- a/internal/service/contract/resource_cosmo_contract.go +++ b/internal/service/contract/resource_cosmo_contract.go @@ -3,6 +3,10 @@ package contract import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" + platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" + "strings" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -27,11 +31,13 @@ type contractResourceModel struct { Name types.String `tfsdk:"name"` SourceGraphName types.String `tfsdk:"source"` Namespace types.String `tfsdk:"namespace"` + RoutingURL types.String `tfsdk:"routing_url"` ExcludeTags types.List `tfsdk:"exclude_tags"` + IncludeTags types.List `tfsdk:"include_tags"` Readme types.String `tfsdk:"readme"` AdmissionWebhookUrl types.String `tfsdk:"admission_webhook_url"` AdmissionWebhookSecret types.String `tfsdk:"admission_webhook_secret"` - RoutingURL types.String `tfsdk:"routing_url"` + SupportsFederation types.Bool `tfsdk:"supports_federation"` } func (r *contractResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -48,6 +54,9 @@ For more information, refer to the Cosmo Documentation at https://cosmo-docs.wun Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "name": schema.StringAttribute{ Required: true, @@ -68,6 +77,10 @@ For more information, refer to the Cosmo Documentation at https://cosmo-docs.wun Optional: true, ElementType: types.StringType, }, + "include_tags": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, "readme": schema.StringAttribute{ Optional: true, }, @@ -80,6 +93,10 @@ For more information, refer to the Cosmo Documentation at https://cosmo-docs.wun "routing_url": schema.StringAttribute{ Required: true, }, + "supports_federation": schema.BoolAttribute{ + Optional: true, + Computed: true, + }, }, } } @@ -109,31 +126,46 @@ func (r *contractResource) Create(ctx context.Context, req resource.CreateReques return } - excludeTags, err := utils.ConvertLabelMatchers(data.ExcludeTags) - if err != nil { - utils.AddDiagnosticError(resp, - ErrCreatingContract, - "Could not create contract: "+err.Error(), - ) - return - } - _, apiError := r.client.CreateContract(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.SourceGraphName.ValueString(), data.RoutingURL.ValueString(), data.AdmissionWebhookUrl.ValueString(), data.AdmissionWebhookSecret.ValueString(), excludeTags, data.Readme.ValueString()) + response, apiError := r.createAndFetchContract(ctx, data, resp) if apiError != nil { - if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, - ErrCreatingContract, - "Contract composition failed: "+apiError.Error(), - ) - } else { - utils.AddDiagnosticError(resp, - ErrCreatingContract, - "Could not create contract: "+apiError.Error(), - ) + if !api.IsSubgraphCompositionFailedError(apiError) { + // returning only for a non composition error, as the resource exists otherwise return } } - data.Id = data.Name + graph := response.Graph + data.Id = types.StringValue(graph.GetId()) + data.Name = types.StringValue(graph.GetName()) + data.Namespace = types.StringValue(graph.GetNamespace()) + data.RoutingURL = types.StringValue(graph.GetRoutingURL()) + data.SupportsFederation = types.BoolValue(graph.GetSupportsFederation()) + + if graph.Contract != nil && len(graph.Contract.GetExcludeTags()) > 0 { + var responseExcludeTags []attr.Value + for _, tag := range graph.Contract.GetExcludeTags() { + responseExcludeTags = append(responseExcludeTags, types.StringValue(tag)) + } + data.ExcludeTags = types.ListValueMust(types.StringType, responseExcludeTags) + } + + if graph.Contract != nil && len(graph.Contract.GetIncludeTags()) > 0 { + var responseIncludeTags []attr.Value + for _, tag := range graph.Contract.IncludeTags { + responseIncludeTags = append(responseIncludeTags, types.StringValue(tag)) + } + data.IncludeTags = types.ListValueMust(types.StringType, responseIncludeTags) + } + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } + + utils.LogAction(ctx, DebugCreate, data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -147,7 +179,7 @@ func (r *contractResource) Read(ctx context.Context, req resource.ReadRequest, r return } - apiResponse, apiError := r.client.GetFederatedGraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + response, apiError := r.client.GetFederatedGraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if apiError != nil { if api.IsNotFoundError(apiError) { utils.AddDiagnosticWarning(resp, @@ -164,11 +196,36 @@ func (r *contractResource) Read(ctx context.Context, req resource.ReadRequest, r return } - graph := apiResponse.Graph + graph := response.Graph data.Id = types.StringValue(graph.GetId()) data.Name = types.StringValue(graph.GetName()) data.Namespace = types.StringValue(graph.GetNamespace()) data.RoutingURL = types.StringValue(graph.GetRoutingURL()) + data.SupportsFederation = types.BoolValue(graph.GetSupportsFederation()) + + if graph.Contract != nil && len(graph.Contract.GetExcludeTags()) > 0 { + var responseExcludeTags []attr.Value + for _, tag := range graph.Contract.GetExcludeTags() { + responseExcludeTags = append(responseExcludeTags, types.StringValue(tag)) + } + data.ExcludeTags = types.ListValueMust(types.StringType, responseExcludeTags) + } + + if graph.Contract != nil && len(graph.Contract.GetIncludeTags()) > 0 { + var responseIncludeTags []attr.Value + for _, tag := range graph.Contract.IncludeTags { + responseIncludeTags = append(responseIncludeTags, types.StringValue(tag)) + } + data.IncludeTags = types.ListValueMust(types.StringType, responseIncludeTags) + } + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } utils.LogAction(ctx, "read", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) @@ -192,10 +249,40 @@ func (r *contractResource) Update(ctx context.Context, req resource.UpdateReques return } - _, apiError := r.client.UpdateContract(ctx, data.Name.ValueString(), data.Namespace.ValueString(), excludeTags) + includeTags, err := utils.ConvertLabelMatchers(data.IncludeTags) + if err != nil { + utils.AddDiagnosticError(resp, + ErrCreatingContract, + "Could not create contract: "+err.Error(), + ) + return + } + + if len(includeTags) > 0 && len(excludeTags) > 0 { + utils.AddDiagnosticError(resp, ErrUpdatingContract, "a contract cannot have both the include and exclude tags") + return + } + + readme := utils.GetValueOrDefault(data.Readme.ValueStringPointer(), "") + admissionWebhookUrl := utils.GetValueOrDefault(data.AdmissionWebhookUrl.ValueStringPointer(), "") + admissionWebhookSecret := utils.GetValueOrDefault(data.AdmissionWebhookSecret.ValueStringPointer(), "") + + routingUrl := data.RoutingURL.ValueString() + requestData := &platformv1.UpdateContractRequest{ + Name: data.Name.ValueString(), + Namespace: data.Namespace.ValueString(), + ExcludeTags: excludeTags, + IncludeTags: includeTags, + RoutingUrl: &routingUrl, + AdmissionWebhookUrl: &admissionWebhookUrl, + AdmissionWebhookSecret: &admissionWebhookSecret, + Readme: &readme, + } + + _, apiError := r.client.UpdateContract(ctx, requestData) if apiError != nil { if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, + utils.AddDiagnosticError(resp, ErrUpdatingContract, apiError.Error(), ) @@ -208,6 +295,46 @@ func (r *contractResource) Update(ctx context.Context, req resource.UpdateReques } } + response, apiError := r.client.GetFederatedGraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrRetrievingContract, + apiError.Error(), + ) + return + } + + graph := response.Graph + data.Id = types.StringValue(graph.GetId()) + data.Name = types.StringValue(graph.GetName()) + data.Namespace = types.StringValue(graph.GetNamespace()) + data.RoutingURL = types.StringValue(graph.GetRoutingURL()) + data.SupportsFederation = types.BoolValue(graph.GetSupportsFederation()) + + if graph.Contract != nil && len(graph.Contract.GetExcludeTags()) > 0 { + var responseExcludeTags []attr.Value + for _, tag := range graph.Contract.GetExcludeTags() { + responseExcludeTags = append(responseExcludeTags, types.StringValue(tag)) + } + data.ExcludeTags = types.ListValueMust(types.StringType, responseExcludeTags) + } + + if graph.Contract != nil && len(graph.Contract.GetIncludeTags()) > 0 { + var responseIncludeTags []attr.Value + for _, tag := range graph.Contract.IncludeTags { + responseIncludeTags = append(responseIncludeTags, types.StringValue(tag)) + } + data.IncludeTags = types.ListValueMust(types.StringType, responseIncludeTags) + } + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -219,15 +346,10 @@ func (r *contractResource) Delete(ctx context.Context, req resource.DeleteReques return } - apiError := r.client.DeleteContract(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + apiError := r.client.DeleteContract(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.SupportsFederation.ValueBool()) if apiError != nil { - if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, - ErrDeletingContract, - apiError.Error(), - ) - } else if api.IsNotFoundError(apiError) { - utils.AddDiagnosticWarning(resp, + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticError(resp, ErrDeletingContract, apiError.Error(), ) @@ -240,8 +362,82 @@ func (r *contractResource) Delete(ctx context.Context, req resource.DeleteReques return } } + + utils.LogAction(ctx, "deleted contract", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) } func (r *contractResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func (r *contractResource) createAndFetchContract(ctx context.Context, data contractResourceModel, resp *resource.CreateResponse) (*platformv1.GetFederatedGraphByNameResponse, *api.ApiError) { + excludeTags, err := utils.ConvertLabelMatchers(data.ExcludeTags) + if err != nil { + utils.AddDiagnosticError(resp, + ErrCreatingContract, + "Could not create contract: "+err.Error(), + ) + return nil, &api.ApiError{Err: err, Reason: "CreateContract", Status: common.EnumStatusCode_ERR} + } + + includeTags, err := utils.ConvertLabelMatchers(data.IncludeTags) + if err != nil { + utils.AddDiagnosticError(resp, + ErrCreatingContract, + "Could not create contract: "+err.Error(), + ) + return nil, &api.ApiError{Err: err, Reason: "CreateContract", Status: common.EnumStatusCode_ERR} + } + + if len(includeTags) > 0 && len(excludeTags) > 0 { + utils.AddDiagnosticError(resp, ErrCreatingContract, "a contract cannot have both the include and exclude tags") + return nil, &api.ApiError{Err: err, Reason: "CreateContract", Status: common.EnumStatusCode_ERR} + } + + utils.DebugAction(ctx, DebugCreate, data.Name.ValueString(), data.Namespace.ValueString(), map[string]interface{}{ + "routing_url": data.RoutingURL.ValueString(), + "excludeTags": strings.Join(excludeTags, ","), + "includeTags": strings.Join(includeTags, ","), + }) + + readme := data.Readme.ValueString() + requestData := &platformv1.CreateContractRequest{ + Name: data.Name.ValueString(), + Namespace: data.Namespace.ValueString(), + SourceGraphName: data.SourceGraphName.ValueString(), + RoutingUrl: data.RoutingURL.ValueString(), + AdmissionWebhookUrl: data.AdmissionWebhookUrl.ValueString(), + ExcludeTags: excludeTags, + Readme: &readme, + AdmissionWebhookSecret: data.AdmissionWebhookSecret.ValueStringPointer(), + IncludeTags: includeTags, + } + + _, apiError := r.client.CreateContract(ctx, requestData) + if apiError != nil { + if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticError(resp, + ErrCreatingContract, + "Contract composition failed: "+apiError.Error(), + ) + } else { + utils.AddDiagnosticError(resp, + ErrCreatingContract, + "Could not create contract: "+apiError.Error(), + ) + return nil, apiError + } + } + + response, apiError := r.client.GetFederatedGraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + return nil, apiError + } + + utils.DebugAction(ctx, DebugCreate, data.Name.ValueString(), data.Namespace.ValueString(), map[string]interface{}{ + "id": response.Graph.GetId(), + "graph": response.Graph, + }) + + return response, nil +} diff --git a/internal/service/contract/resource_cosmo_contract_test.go b/internal/service/contract/resource_cosmo_contract_test.go index 2e17db2..4c20a33 100644 --- a/internal/service/contract/resource_cosmo_contract_test.go +++ b/internal/service/contract/resource_cosmo_contract_test.go @@ -16,45 +16,155 @@ func TestAccContractResource(t *testing.T) { readme := "Initial readme content" federatedGraphName := acctest.RandomWithPrefix("test-federated-graph") - federatedGraphroutingURL := "https://example.com:3000" - graphUrl := "http://example.com/graphql" + subgraphName := acctest.RandomWithPrefix("test-subgraph") resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccContractResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, graphUrl, name, readme), + Config: testAccContractResourceConfig(namespace, federatedGraphName, subgraphName, name, "internal", &readme), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_contract.test", "name", name), resource.TestCheckResourceAttr("cosmo_contract.test", "namespace", namespace), resource.TestCheckResourceAttr("cosmo_contract.test", "readme", readme), + resource.TestCheckResourceAttr("cosmo_contract.test", "readme", readme), + resource.TestCheckResourceAttr("cosmo_contract.test", "exclude_tags.0", "internal"), + ), + }, + { + Config: testAccContractResourceConfig(namespace, federatedGraphName, subgraphName, name, "external", &readme), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_contract.test", "name", name), + resource.TestCheckResourceAttr("cosmo_contract.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_contract.test", "readme", readme), + resource.TestCheckResourceAttr("cosmo_contract.test", "exclude_tags.0", "external"), + ), + }, + { + ResourceName: "cosmo_contract.test", + RefreshState: true, + }, + { + Config: testAccContractResourceConfig(namespace, federatedGraphName, subgraphName, name, "external", &readme), + Destroy: true, + }, + { + Config: testAccContractOfMonographResourceConfig(namespace, federatedGraphName, name, readme), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_contract.test_mono", "name", name), + resource.TestCheckResourceAttr("cosmo_contract.test_mono", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_contract.test_mono", "readme", readme), + ), + }, + { + Config: testAccContractOfMonographResourceConfig(namespace, federatedGraphName, name, readme), + Destroy: true, + }, + }, + }) +} + +func TestOptionalValuesOfContractResource(t *testing.T) { + name := acctest.RandomWithPrefix("test-contract") + namespace := acctest.RandomWithPrefix("test-namespace") + + federatedGraphName := acctest.RandomWithPrefix("test-federated-graph") + + subgraphName := acctest.RandomWithPrefix("test-subgraph") + + readme := "Initial readme content" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccContractResourceConfig(namespace, federatedGraphName, subgraphName, name, "internal", &readme), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_contract.test", "name", name), + resource.TestCheckResourceAttr("cosmo_contract.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_contract.test", "exclude_tags.0", "internal"), + resource.TestCheckResourceAttr("cosmo_contract.test", "readme", readme), + ), + }, + { + Config: testAccContractResourceConfig(namespace, federatedGraphName, subgraphName, name, "external", nil), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_contract.test", "name", name), + resource.TestCheckResourceAttr("cosmo_contract.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_contract.test", "exclude_tags.0", "external"), + resource.TestCheckNoResourceAttr("cosmo_contract.test", "readme"), ), }, }, }) } -func testAccContractResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, graphUrl, contractName, contractReadme string) string { +func testAccContractResourceConfig(namespace, federatedGraphName, subgraphName, contractName, contractExcludeTag string, contractReadme *string) string { + var readmePart string + if contractReadme != nil { + readmePart = fmt.Sprintf(`readme = "%s"`, *contractReadme) + } + subgraphSchema := acceptance.TestAccValidSubgraphSchema + return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" } -resource "cosmo_monograph" "test" { +resource "cosmo_federated_graph" "test" { name = "%s" namespace = cosmo_namespace.test.name - routing_url = "%s" - graph_url = "%s" + routing_url = "https://example.com:3000" + depends_on = [cosmo_subgraph.test] +} + +resource "cosmo_subgraph" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + routing_url = "https://subgraph-standalone-example.com" + schema = <<-EOT + %s + EOT + labels = {} } resource "cosmo_contract" "test" { name = "%s" namespace = cosmo_namespace.test.name - source = cosmo_monograph.test.name + source = cosmo_federated_graph.test.name + routing_url = "http://localhost:3003" + exclude_tags = ["%s"] + %s +} +`, namespace, federatedGraphName, subgraphName, subgraphSchema, contractName, contractExcludeTag, readmePart) +} + +func testAccContractOfMonographResourceConfig(namespace, federatedGraphName, contractName, contractReadme string) string { + schema := acceptance.TestAccValidSubgraphSchema + return fmt.Sprintf(` +resource "cosmo_namespace" "test_mono" { + name = "%s" +} + +resource "cosmo_monograph" "test_mono" { + name = "%s" + namespace = cosmo_namespace.test_mono.name + routing_url = "https://example.com:3000" + graph_url = "https://subgraph-standalone-example.com" + schema = <<-EOT + %s + EOT +} + +resource "cosmo_contract" "test_mono" { + name = "%s" + namespace = cosmo_namespace.test_mono.name + source = cosmo_monograph.test_mono.name routing_url = "http://localhost:3003" readme = "%s" } -`, namespace, federatedGraphName, federatedGraphroutingURL, graphUrl, contractName, contractReadme) +`, namespace, federatedGraphName, schema, contractName, contractReadme) } diff --git a/internal/service/federated-graph/data_source_cosmo_federated_graph.go b/internal/service/federated-graph/data_source_cosmo_federated_graph.go index 82fbf8f..375d566 100644 --- a/internal/service/federated-graph/data_source_cosmo_federated_graph.go +++ b/internal/service/federated-graph/data_source_cosmo_federated_graph.go @@ -131,6 +131,10 @@ func (d *FederatedGraphDataSource) Read(ctx context.Context, req datasource.Read data.Readme = types.StringValue(*graph.Readme) } + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } + tflog.Trace(ctx, "Read federated graph data source", map[string]interface{}{ "id": data.Id.ValueString(), }) diff --git a/internal/service/federated-graph/data_source_cosmo_federated_graph_test.go b/internal/service/federated-graph/data_source_cosmo_federated_graph_test.go index ceb1e89..2d5da9d 100644 --- a/internal/service/federated-graph/data_source_cosmo_federated_graph_test.go +++ b/internal/service/federated-graph/data_source_cosmo_federated_graph_test.go @@ -28,6 +28,10 @@ func TestAccFederatedGraphDataSource(t *testing.T) { ResourceName: "data.cosmo_federated_graph.test", RefreshState: true, }, + { + Config: testAccFederatedGraphDataSourceConfig(namespace, name), + Destroy: true, + }, }, }) } diff --git a/internal/service/federated-graph/resource_cosmo_federated_graph.go b/internal/service/federated-graph/resource_cosmo_federated_graph.go index 1edec55..0643054 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph.go @@ -39,11 +39,11 @@ type FederatedGraphResourceModel struct { Id types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Namespace types.String `tfsdk:"namespace"` - Readme types.String `tfsdk:"readme"` RoutingURL types.String `tfsdk:"routing_url"` + LabelMatchers types.List `tfsdk:"label_matchers"` AdmissionWebhookUrl types.String `tfsdk:"admission_webhook_url"` AdmissionWebhookSecret types.String `tfsdk:"admission_webhook_secret"` - LabelMatchers types.List `tfsdk:"label_matchers"` + Readme types.String `tfsdk:"readme"` } func (r *FederatedGraphResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -138,10 +138,6 @@ func (r *FederatedGraphResource) Create(ctx context.Context, req resource.Create response, apiError := r.createFederatedGraph(ctx, data, resp) if apiError != nil { if !api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticError(resp, - ErrCreatingGraph, - apiError.Error(), - ) return } } @@ -152,6 +148,20 @@ func (r *FederatedGraphResource) Create(ctx context.Context, req resource.Create data.Namespace = types.StringValue(graph.GetNamespace()) data.RoutingURL = types.StringValue(graph.GetRoutingURL()) + var responseLabelMatchers []attr.Value + for _, matcher := range graph.LabelMatchers { + responseLabelMatchers = append(responseLabelMatchers, types.StringValue(matcher)) + } + data.LabelMatchers = types.ListValueMust(types.StringType, responseLabelMatchers) + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } + utils.LogAction(ctx, DebugCreate, data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -190,11 +200,19 @@ func (r *FederatedGraphResource) Read(ctx context.Context, req resource.ReadRequ data.Namespace = types.StringValue(graph.GetNamespace()) data.RoutingURL = types.StringValue(graph.GetRoutingURL()) - var labelMatchers []attr.Value + var responseLabelMatchers []attr.Value for _, matcher := range graph.LabelMatchers { - labelMatchers = append(labelMatchers, types.StringValue(matcher)) + responseLabelMatchers = append(responseLabelMatchers, types.StringValue(matcher)) + } + data.LabelMatchers = types.ListValueMust(types.StringType, responseLabelMatchers) + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) } - data.LabelMatchers = types.ListValueMust(types.StringType, labelMatchers) utils.LogAction(ctx, "read", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) @@ -219,24 +237,23 @@ func (r *FederatedGraphResource) Update(ctx context.Context, req resource.Update return } - graph := platformv1.FederatedGraph{ + readme := utils.GetValueOrDefault(data.Readme.ValueStringPointer(), "") + admissionWebhookUrl := utils.GetValueOrDefault(data.AdmissionWebhookUrl.ValueStringPointer(), "") + admissionWebhookSecret := utils.GetValueOrDefault(data.AdmissionWebhookSecret.ValueStringPointer(), "") + + updatedGraph := platformv1.FederatedGraph{ Name: data.Name.ValueString(), Namespace: data.Namespace.ValueString(), RoutingURL: data.RoutingURL.ValueString(), - AdmissionWebhookUrl: data.AdmissionWebhookUrl.ValueStringPointer(), + AdmissionWebhookUrl: &admissionWebhookUrl, LabelMatchers: labelMatchers, - Readme: data.Readme.ValueStringPointer(), + Readme: &readme, } - var admissionWebhookSecret *string - if !data.AdmissionWebhookSecret.IsNull() { - admissionWebhookSecret = data.AdmissionWebhookSecret.ValueStringPointer() - } - - _, apiError := r.client.UpdateFederatedGraph(ctx, admissionWebhookSecret, &graph) + _, apiError := r.client.UpdateFederatedGraph(ctx, &admissionWebhookSecret, &updatedGraph) if apiError != nil { if api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, + utils.AddDiagnosticError(resp, ErrCompositionError, apiError.Error(), ) @@ -249,7 +266,36 @@ func (r *FederatedGraphResource) Update(ctx context.Context, req resource.Update } } - utils.LogAction(ctx, "updated", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + utils.LogAction(ctx, "updated federated graph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + + response, apiError := r.client.GetFederatedGraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrRetrievingGraph, + apiError.Error(), + ) + return + } + + graph := response.Graph + data.Id = types.StringValue(graph.GetId()) + data.Name = types.StringValue(graph.GetName()) + data.Namespace = types.StringValue(graph.GetNamespace()) + data.RoutingURL = types.StringValue(graph.GetRoutingURL()) + + var responseLabelMatchers []attr.Value + for _, matcher := range graph.LabelMatchers { + responseLabelMatchers = append(responseLabelMatchers, types.StringValue(matcher)) + } + data.LabelMatchers = types.ListValueMust(types.StringType, responseLabelMatchers) + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + if graph.GetAdmissionWebhookUrl() != "" { + data.AdmissionWebhookUrl = types.StringValue(*graph.AdmissionWebhookUrl) + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -270,14 +316,22 @@ func (r *FederatedGraphResource) Delete(ctx context.Context, req resource.Delete apiError := r.client.DeleteFederatedGraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if apiError != nil { - utils.AddDiagnosticError(resp, - ErrDeletingGraph, - apiError.Error(), - ) - return + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticError(resp, + ErrDeletingGraph, + apiError.Error(), + ) + resp.State.RemoveResource(ctx) + } else { + utils.AddDiagnosticError(resp, + ErrDeletingGraph, + apiError.Error(), + ) + return + } } - utils.LogAction(ctx, "deleted", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + utils.LogAction(ctx, "deleted federated graph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) } func (r *FederatedGraphResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -313,8 +367,12 @@ func (r *FederatedGraphResource) createFederatedGraph(ctx context.Context, data _, apiError := r.client.CreateFederatedGraph(ctx, admissionWebhookSecret, &apiGraph) if apiError != nil { if api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, ErrCompositionError, apiError.Error()) + utils.AddDiagnosticError(resp, ErrCreatingGraph, apiError.Error()) } else { + utils.AddDiagnosticError(resp, + ErrCreatingGraph, + "Could not create federated graph: "+apiError.Error(), + ) return nil, apiError } } diff --git a/internal/service/federated-graph/resource_cosmo_federated_graph_test.go b/internal/service/federated-graph/resource_cosmo_federated_graph_test.go index 295cd8e..8543c88 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph_test.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph_test.go @@ -25,7 +25,7 @@ func TestAccFederatedGraphResource(t *testing.T) { ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccFederatedGraphResourceConfig(namespace, name, routingURL, readme), + Config: testAccFederatedGraphResourceConfig(namespace, name, routingURL, &readme), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_federated_graph.test", "name", name), resource.TestCheckResourceAttr("cosmo_federated_graph.test", "namespace", namespace), @@ -34,21 +34,26 @@ func TestAccFederatedGraphResource(t *testing.T) { ), }, { - Config: testAccFederatedGraphResourceConfig(namespace, name, routingURL, newReadme), + Config: testAccFederatedGraphResourceConfig(namespace, name, routingURL, &newReadme), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_federated_graph.test", "readme", newReadme), ), }, { - Config: testAccFederatedGraphResourceConfig(namespace, name, updatedRoutingURL, newReadme), + Config: testAccFederatedGraphResourceConfig(namespace, name, updatedRoutingURL, nil), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_federated_graph.test", "routing_url", updatedRoutingURL), + resource.TestCheckNoResourceAttr("cosmo_federated_graph.test", "readme"), ), }, { ResourceName: "cosmo_federated_graph.test", RefreshState: true, }, + { + Config: testAccFederatedGraphResourceConfig(namespace, name, updatedRoutingURL, nil), + Destroy: true, + }, }, }) } @@ -56,20 +61,26 @@ func TestAccFederatedGraphResource(t *testing.T) { func TestAccFederatedGraphResourceInvalidConfig(t *testing.T) { name := acctest.RandomWithPrefix("test-federated-graph") namespace := acctest.RandomWithPrefix("test-namespace") + readme := "Initial readme content" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccFederatedGraphResourceConfig(name, namespace, "invalid-url", ""), + Config: testAccFederatedGraphResourceConfig(name, namespace, "invalid-url", &readme), ExpectError: regexp.MustCompile(`.*Routing URL is not a valid URL*`), }, }, }) } -func testAccFederatedGraphResourceConfig(namespace, name, routingURL, readme string) string { +func testAccFederatedGraphResourceConfig(namespace, name, routingURL string, readme *string) string { + var readmePart string + if readme != nil { + readmePart = fmt.Sprintf(`readme = "%s"`, *readme) + } + return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" @@ -79,7 +90,7 @@ resource "cosmo_federated_graph" "test" { name = "%s" namespace = cosmo_namespace.test.name routing_url = "%s" - readme = "%s" + %s } -`, namespace, name, routingURL, readme) +`, namespace, name, routingURL, readmePart) } diff --git a/internal/service/monograph/errors.go b/internal/service/monograph/errors.go index 04d41dd..d3fd061 100644 --- a/internal/service/monograph/errors.go +++ b/internal/service/monograph/errors.go @@ -3,6 +3,7 @@ package monograph const ( ErrInvalidMonographName = "Invalid Monograph Name" ErrCreatingMonograph = "Error Creating Monograph" + ErrPublishingMonograph = "Error Publishing Monograph" ErrCompositionError = "Composition Error" ErrRetrievingMonograph = "Error Retrieving Monograph" ErrInvalidResourceID = "Invalid Resource ID" diff --git a/internal/service/monograph/resource_cosmo_monograph.go b/internal/service/monograph/resource_cosmo_monograph.go index 2b79107..995be6e 100644 --- a/internal/service/monograph/resource_cosmo_monograph.go +++ b/internal/service/monograph/resource_cosmo_monograph.go @@ -33,6 +33,7 @@ type MonographResourceModel struct { Readme types.String `tfsdk:"readme"` AdmissionWebhookURL types.String `tfsdk:"admission_webhook_url"` AdmissionWebhookSecret types.String `tfsdk:"admission_webhook_secret"` + Schema types.String `tfsdk:"schema"` } func NewMonographResource() resource.Resource { @@ -54,6 +55,9 @@ For more information on monographs, please refer to the [Cosmo Documentation](ht "id": schema.StringAttribute{ Computed: true, MarkdownDescription: "The unique identifier of the monograph resource.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "name": schema.StringAttribute{ Required: true, @@ -109,6 +113,10 @@ For more information on monographs, please refer to the [Cosmo Documentation](ht stringvalidator.OneOf(api.GraphQLSubscriptionProtocolWS, api.GraphQLSubscriptionProtocolSSE, api.GraphQLSubscriptionProtocolSSEPost), }, }, + "schema": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The schema for the subgraph.", + }, }, } } @@ -167,6 +175,26 @@ func (r *MonographResource) Create(ctx context.Context, req resource.CreateReque return } + if data.Schema.ValueString() != "" { + err := r.client.PublishMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) + if err != nil { + if api.IsNotFoundError(err) { + utils.AddDiagnosticError(resp, + ErrPublishingMonograph, + err.Error(), + ) + resp.State.RemoveResource(ctx) + return + } else { + utils.AddDiagnosticError(resp, + ErrPublishingMonograph, + err.Error(), + ) + return + } + } + } + monograph, apiError := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if apiError != nil { utils.AddDiagnosticError(resp, @@ -181,7 +209,7 @@ func (r *MonographResource) Create(ctx context.Context, req resource.CreateReque data.Readme = types.StringValue(*monograph.Readme) } - utils.LogAction(ctx, "created", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + utils.LogAction(ctx, "created monograph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -220,7 +248,7 @@ func (r *MonographResource) Read(ctx context.Context, req resource.ReadRequest, data.Readme = types.StringValue(*monograph.Readme) } - utils.LogAction(ctx, "read", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + utils.LogAction(ctx, "read monograph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -247,13 +275,44 @@ func (r *MonographResource) Update(ctx context.Context, req resource.UpdateReque data.AdmissionWebhookSecret.ValueString(), ) if err != nil { - utils.AddDiagnosticError(resp, - ErrUpdatingMonograph, - err.Error(), - ) - return + if api.IsNotFoundError(err) { + utils.AddDiagnosticError(resp, + ErrUpdatingMonograph, + err.Error(), + ) + resp.State.RemoveResource(ctx) + return + } else { + utils.AddDiagnosticError(resp, + ErrUpdatingMonograph, + err.Error(), + ) + return + } } + if data.Schema.ValueString() != "" { + err := r.client.PublishMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) + if err != nil { + if api.IsNotFoundError(err) { + utils.AddDiagnosticError(resp, + ErrUpdatingMonograph, + err.Error(), + ) + resp.State.RemoveResource(ctx) + return + } else { + utils.AddDiagnosticError(resp, + ErrUpdatingMonograph, + err.Error(), + ) + return + } + } + } + + utils.LogAction(ctx, "updated monograph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + monograph, err := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if err != nil { utils.AddDiagnosticError(resp, @@ -266,8 +325,6 @@ func (r *MonographResource) Update(ctx context.Context, req resource.UpdateReque data.Id = types.StringValue(monograph.GetId()) data.Name = types.StringValue(monograph.GetName()) - utils.LogAction(ctx, "updated", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -281,14 +338,22 @@ func (r *MonographResource) Delete(ctx context.Context, req resource.DeleteReque apiError := r.client.DeleteMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if apiError != nil { - utils.AddDiagnosticError(resp, - ErrDeletingMonograph, - apiError.Error(), - ) - return + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticError(resp, + ErrDeletingMonograph, + apiError.Error(), + ) + resp.State.RemoveResource(ctx) + } else { + utils.AddDiagnosticError(resp, + ErrDeletingMonograph, + apiError.Error(), + ) + return + } } - utils.LogAction(ctx, "deleted", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + utils.LogAction(ctx, "deleted monograph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) } func (r *MonographResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/internal/service/subgraph/data_source_cosmo_subgraph.go b/internal/service/subgraph/data_source_cosmo_subgraph.go index 4e95edb..7bc9228 100644 --- a/internal/service/subgraph/data_source_cosmo_subgraph.go +++ b/internal/service/subgraph/data_source_cosmo_subgraph.go @@ -179,6 +179,11 @@ func (d *SubgraphDataSource) Read(ctx context.Context, req datasource.ReadReques data.IsFeatureSubgraph = types.BoolValue(subgraph.GetIsFeatureSubgraph()) data.SubscriptionProtocol = types.StringValue(subgraph.GetSubscriptionProtocol()) data.WebsocketSubprotocol = types.StringValue(subgraph.GetWebsocketSubprotocol()) + data.IsEventDrivenGraph = types.BoolValue(subgraph.GetIsEventDrivenGraph()) + + if subgraph.GetSubscriptionUrl() != "" { + data.SubscriptionUrl = types.StringValue(subgraph.GetSubscriptionUrl()) + } tflog.Trace(ctx, "Read subgraph data source", map[string]interface{}{ "id": data.Id.ValueString(), diff --git a/internal/service/subgraph/resource_cosmo_subgraph.go b/internal/service/subgraph/resource_cosmo_subgraph.go index 2a81815..61e80ea 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph.go +++ b/internal/service/subgraph/resource_cosmo_subgraph.go @@ -3,6 +3,7 @@ package subgraph import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -79,6 +80,9 @@ For more information on subgraphs, please refer to the [Cosmo Documentation](htt "id": schema.StringAttribute{ Computed: true, MarkdownDescription: "The unique identifier of the subgraph resource.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "name": schema.StringAttribute{ Required: true, @@ -107,6 +111,8 @@ For more information on subgraphs, please refer to the [Cosmo Documentation](htt Validators: []validator.String{ stringvalidator.OneOf(api.GraphQLSubscriptionProtocolWS, api.GraphQLSubscriptionProtocolSSE, api.GraphQLSubscriptionProtocolSSEPost), }, + Computed: true, + Default: stringdefault.StaticString(api.GraphQLSubscriptionProtocolWS), }, "readme": schema.StringAttribute{ Optional: true, @@ -118,10 +124,14 @@ For more information on subgraphs, please refer to the [Cosmo Documentation](htt Validators: []validator.String{ stringvalidator.OneOf(api.GraphQLWebsocketSubprotocolDefault, api.GraphQLWebsocketSubprotocolGraphQLWS, api.GraphQLWebsocketSubprotocolGraphQLTransportWS), }, + Computed: true, + Default: stringdefault.StaticString(api.GraphQLWebsocketSubprotocolDefault), }, "is_event_driven_graph": schema.BoolAttribute{ Optional: true, MarkdownDescription: "Indicates if the subgraph is event-driven.", + Computed: true, + Default: booldefault.StaticBool(false), }, "is_feature_subgraph": schema.BoolAttribute{ Optional: true, @@ -140,6 +150,7 @@ For more information on subgraphs, please refer to the [Cosmo Documentation](htt Optional: true, MarkdownDescription: "Labels for the subgraph.", ElementType: types.StringType, + Computed: true, }, "schema": schema.StringAttribute{ Optional: true, @@ -164,15 +175,22 @@ func (r *SubgraphResource) Create(ctx context.Context, req resource.CreateReques subgraph, apiError := r.createAndPublishSubgraph(ctx, data, resp) if apiError != nil { - if api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, ErrSubgraphCompositionFailed, apiError.Error()) - } else if api.IsInvalidSubgraphSchemaError(apiError) { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, apiError.Error()) + if !api.IsSubgraphCompositionFailedError(apiError) { return - } else { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, apiError.Error()) + } + } + + subgraphSchema, apiError := r.client.GetSubgraphSchema(ctx, subgraph.Name, subgraph.Namespace) + if apiError != nil { + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrSubgraphNotFound, + fmt.Sprintf("Subgraph '%s' not found will be recreated %s", data.Name.ValueString(), apiError.Error()), + ) + resp.State.RemoveResource(ctx) return } + utils.AddDiagnosticError(resp, ErrRetrievingSubgraph, fmt.Sprintf("Could not fetch subgraph '%s': %s", data.Name.ValueString(), apiError.Error())) return } @@ -180,8 +198,35 @@ func (r *SubgraphResource) Create(ctx context.Context, req resource.CreateReques data.Name = types.StringValue(subgraph.GetName()) data.Namespace = types.StringValue(subgraph.GetNamespace()) data.RoutingURL = types.StringValue(subgraph.GetRoutingURL()) + data.SubscriptionProtocol = types.StringValue(subgraph.GetSubscriptionProtocol()) + data.WebsocketSubprotocol = types.StringValue(subgraph.GetWebsocketSubprotocol()) + data.IsEventDrivenGraph = types.BoolValue(subgraph.GetIsEventDrivenGraph()) + labels := map[string]attr.Value{} + for _, label := range subgraph.GetLabels() { + if label != nil { + labels[label.GetKey()] = types.StringValue(label.GetValue()) + } + } + mapValue, _ := types.MapValueFrom(ctx, types.StringType, labels) + if len(labels) != 0 { + data.Labels = mapValue + } else { + data.Labels = types.MapValueMust(types.StringType, map[string]attr.Value{}) + } - utils.LogAction(ctx, "created", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + if subgraph.GetSubscriptionUrl() != "" { + data.SubscriptionUrl = types.StringValue(subgraph.GetSubscriptionUrl()) + } + + if subgraph.Readme != nil { + data.Readme = types.StringValue(subgraph.GetReadme()) + } + + if len(subgraphSchema) > 0 { + data.Schema = types.StringValue(subgraphSchema) + } + + utils.LogAction(ctx, "created subgraph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -232,7 +277,7 @@ func (r *SubgraphResource) Read(ctx context.Context, req resource.ReadRequest, r } } - schema, apiError := r.client.GetSubgraphSchema(ctx, subgraph.Name, subgraph.Namespace) + subgraphSchema, apiError := r.client.GetSubgraphSchema(ctx, subgraph.Name, subgraph.Namespace) if apiError != nil { if api.IsNotFoundError(apiError) { utils.AddDiagnosticWarning(resp, @@ -258,12 +303,28 @@ func (r *SubgraphResource) Read(ctx context.Context, req resource.ReadRequest, r data.Name = types.StringValue(subgraph.GetName()) data.Namespace = types.StringValue(subgraph.GetNamespace()) data.RoutingURL = types.StringValue(subgraph.GetRoutingURL()) - if len(schema) > 0 { - data.Schema = types.StringValue(schema) + data.SubscriptionProtocol = types.StringValue(subgraph.GetSubscriptionProtocol()) + data.WebsocketSubprotocol = types.StringValue(subgraph.GetWebsocketSubprotocol()) + data.IsEventDrivenGraph = types.BoolValue(subgraph.GetIsEventDrivenGraph()) + if len(labels) != 0 { + data.Labels = mapValue + } else { + data.Labels = types.MapValueMust(types.StringType, map[string]attr.Value{}) + } + + if subgraph.GetSubscriptionUrl() != "" { + data.SubscriptionUrl = types.StringValue(subgraph.GetSubscriptionUrl()) + } + + if subgraph.Readme != nil { + data.Readme = types.StringValue(subgraph.GetReadme()) } - data.Labels = mapValue - utils.LogAction(ctx, "read", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + if len(subgraphSchema) > 0 { + data.Schema = types.StringValue(subgraphSchema) + } + + utils.LogAction(ctx, "read subgraph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -287,19 +348,45 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques } var unsetLabels *bool - if data.UnsetLabels.ValueBool() { + if len(labels) == 0 || data.UnsetLabels.ValueBool() { unsetLabels = &[]bool{true}[0] } + readme := utils.GetValueOrDefault(data.Readme.ValueStringPointer(), "") + subscriptionUrl := utils.GetValueOrDefault(data.SubscriptionUrl.ValueStringPointer(), "") + subscriptionProtocol := utils.GetValueOrDefault(data.SubscriptionProtocol.ValueStringPointer(), api.GraphQLSubscriptionProtocolWS) + websocketSubprotocol := utils.GetValueOrDefault(data.WebsocketSubprotocol.ValueStringPointer(), api.GraphQLWebsocketSubprotocolDefault) + + routingUrl := data.RoutingURL.ValueString() + requestData := &platformv1.UpdateSubgraphRequest{ + Name: data.Name.ValueString(), + RoutingUrl: &routingUrl, + Namespace: data.Namespace.ValueString(), + Labels: labels, + UnsetLabels: unsetLabels, + SubscriptionUrl: &subscriptionUrl, + SubscriptionProtocol: api.ResolveSubscriptionProtocol(subscriptionProtocol), + WebsocketSubprotocol: api.ResolveWebsocketSubprotocol(websocketSubprotocol), + Readme: &readme, + Headers: []string{}, + } + // TBD: This is only used in the update subgraph method and not used atm // headers := utils.ConvertHeadersToStringList(data.Headers) - apiErr := r.client.UpdateSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.RoutingURL.ValueString(), labels, []string{}, data.SubscriptionUrl.ValueStringPointer(), data.Readme.ValueStringPointer(), unsetLabels, data.SubscriptionProtocol.ValueString(), data.WebsocketSubprotocol.ValueString()) + apiErr := r.client.UpdateSubgraph(ctx, requestData) if apiErr != nil { if api.IsSubgraphCompositionFailedError(apiErr) { utils.AddDiagnosticWarning(resp, ErrSubgraphCompositionFailed, apiErr.Error(), ) + } else if api.IsNotFoundError(apiErr) { + utils.AddDiagnosticError(resp, + ErrUpdatingSubgraph, + apiErr.Error(), + ) + resp.State.RemoveResource(ctx) + return } else { utils.AddDiagnosticError(resp, ErrUpdatingSubgraph, @@ -309,6 +396,25 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques } } + if data.Schema.ValueString() != "" { + err := r.publishSubgraphSchema(ctx, data) + if err != nil { + if api.IsNotFoundError(err) { + utils.AddDiagnosticError(resp, + ErrUpdatingSubgraph, + err.Error(), + ) + resp.State.RemoveResource(ctx) + return + } else if api.IsSubgraphCompositionFailedError(err) { + utils.AddDiagnosticError(resp, ErrSubgraphCompositionFailed, err.Error()) + } else { + utils.AddDiagnosticError(resp, ErrPublishingSubgraph, err.Error()) + return + } + } + } + subgraph, err := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if err != nil { utils.AddDiagnosticError(resp, @@ -318,32 +424,52 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques return } - if data.Schema.ValueString() != "" { - hasChanged, apiError := r.publishSubgraphSchema(ctx, data) - if apiError != nil { - if api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, ErrPublishingSubgraph, apiError.Error()) - } else if api.IsInvalidSubgraphSchemaError(apiError) { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, apiError.Error()) - return - } else { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, apiError.Error()) - return - } - } - - if hasChanged { + subgraphSchema, apiError := r.client.GetSubgraphSchema(ctx, subgraph.Name, subgraph.Namespace) + if apiError != nil { + if api.IsNotFoundError(apiError) { utils.AddDiagnosticWarning(resp, - ErrSubgraphSchemaChanged, - "The schema has changed", + ErrSubgraphNotFound, + fmt.Sprintf("Subgraph '%s' not found will be recreated %s", data.Name.ValueString(), apiError.Error()), ) + resp.State.RemoveResource(ctx) + return + } + utils.AddDiagnosticError(resp, ErrRetrievingSubgraph, fmt.Sprintf("Could not fetch subgraph '%s': %s", data.Name.ValueString(), apiError.Error())) + return + } + responseLabels := map[string]attr.Value{} + for _, label := range subgraph.GetLabels() { + if label != nil { + responseLabels[label.GetKey()] = types.StringValue(label.GetValue()) } } + mapValue, diags := types.MapValueFrom(ctx, types.StringType, responseLabels) + resp.Diagnostics.Append(diags...) data.Id = types.StringValue(subgraph.GetId()) data.Name = types.StringValue(subgraph.GetName()) data.Namespace = types.StringValue(subgraph.GetNamespace()) data.RoutingURL = types.StringValue(subgraph.GetRoutingURL()) + data.SubscriptionProtocol = types.StringValue(subgraph.GetSubscriptionProtocol()) + data.WebsocketSubprotocol = types.StringValue(subgraph.GetWebsocketSubprotocol()) + data.IsEventDrivenGraph = types.BoolValue(subgraph.GetIsEventDrivenGraph()) + if len(responseLabels) != 0 { + data.Labels = mapValue + } else { + data.Labels = types.MapValueMust(types.StringType, map[string]attr.Value{}) + } + + if subgraph.GetSubscriptionUrl() != "" { + data.SubscriptionUrl = types.StringValue(subgraph.GetSubscriptionUrl()) + } + + if subgraph.Readme != nil { + data.Readme = types.StringValue(subgraph.GetReadme()) + } + + if len(subgraphSchema) > 0 { + data.Schema = types.StringValue(subgraphSchema) + } utils.LogAction(ctx, "updated", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) @@ -365,7 +491,12 @@ func (r *SubgraphResource) Delete(ctx context.Context, req resource.DeleteReques ErrDeletingSubgraph, apiErr.Error(), ) - return + } else if api.IsNotFoundError(apiErr) { + utils.AddDiagnosticError(resp, + ErrDeletingSubgraph, + apiErr.Error(), + ) + resp.State.RemoveResource(ctx) } else { utils.AddDiagnosticError(resp, ErrDeletingSubgraph, @@ -375,7 +506,7 @@ func (r *SubgraphResource) Delete(ctx context.Context, req resource.DeleteReques } } - utils.LogAction(ctx, "deleted", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) + utils.LogAction(ctx, "deleted subgraph", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) } func (r *SubgraphResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -393,7 +524,20 @@ func (r *SubgraphResource) createAndPublishSubgraph(ctx context.Context, data Su } } - apiErr := r.client.CreateSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.RoutingURL.ValueString(), nil, labels, data.SubscriptionUrl.ValueStringPointer(), data.Readme.ValueStringPointer(), data.IsEventDrivenGraph.ValueBoolPointer(), data.IsFeatureSubgraph.ValueBoolPointer(), data.SubscriptionProtocol.ValueString(), data.WebsocketSubprotocol.ValueString()) + routingUrl := data.RoutingURL.ValueString() + requestData := &platformv1.CreateFederatedSubgraphRequest{ + Name: data.Name.ValueString(), + Namespace: data.Namespace.ValueString(), + RoutingUrl: &routingUrl, + Labels: labels, + SubscriptionUrl: data.SubscriptionUrl.ValueStringPointer(), + Readme: data.Readme.ValueStringPointer(), + SubscriptionProtocol: api.ResolveSubscriptionProtocol(data.SubscriptionProtocol.ValueString()), + WebsocketSubprotocol: api.ResolveWebsocketSubprotocol(data.WebsocketSubprotocol.ValueString()), + IsEventDrivenGraph: data.IsEventDrivenGraph.ValueBoolPointer(), + } + + apiErr := r.client.CreateSubgraph(ctx, requestData) if apiErr != nil { utils.AddDiagnosticError(resp, ErrCreatingSubgraph, @@ -402,45 +546,42 @@ func (r *SubgraphResource) createAndPublishSubgraph(ctx context.Context, data Su return nil, apiErr } - subgraph, apiErr := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if apiErr != nil { - return nil, apiErr - } - if data.Schema.ValueString() != "" { - hasChanged, apiError := r.publishSubgraphSchema(ctx, data) + apiError := r.publishSubgraphSchema(ctx, data) if apiError != nil { - if api.IsSubgraphCompositionFailedError(apiError) { - utils.AddDiagnosticWarning(resp, ErrSubgraphCompositionFailed, apiError.Error()) - } else if api.IsInvalidSubgraphSchemaError(apiError) { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, apiError.Error()) + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticError(resp, + ErrUpdatingSubgraph, + apiError.Error(), + ) + resp.State.RemoveResource(ctx) return nil, apiError + } else if api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticError(resp, ErrSubgraphCompositionFailed, apiError.Error()) } else { utils.AddDiagnosticError(resp, ErrPublishingSubgraph, apiError.Error()) return nil, apiError } } + } - if hasChanged { - utils.AddDiagnosticWarning(resp, - ErrSubgraphSchemaChanged, - "The schema has changed", - ) - } + subgraph, apiErr := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiErr != nil { + return nil, apiErr } return subgraph, nil } -func (r *SubgraphResource) publishSubgraphSchema(ctx context.Context, data SubgraphResourceModel) (bool, *api.ApiError) { +func (r *SubgraphResource) publishSubgraphSchema(ctx context.Context, data SubgraphResourceModel) *api.ApiError { apiResponse, apiError := r.client.PublishSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) if apiError != nil { - return false, apiError + return apiError } if apiResponse != nil && apiResponse.HasChanged != nil && *apiResponse.HasChanged { - return true, nil + return nil } - return false, nil + return nil } diff --git a/internal/service/subgraph/resource_cosmo_subgraph_test.go b/internal/service/subgraph/resource_cosmo_subgraph_test.go index cf0950c..7171d6f 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph_test.go +++ b/internal/service/subgraph/resource_cosmo_subgraph_test.go @@ -2,6 +2,7 @@ package subgraph_test import ( "fmt" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/api" "regexp" "testing" @@ -22,23 +23,27 @@ func TestAccSubgraphResource(t *testing.T) { updatedRoutingURL := "https://updated-subgraph-example.com" subgraphSchema := acceptance.TestAccValidSubgraphSchema + readme := "Initial readme content" resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema, readme), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", routingURL), resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.team", "backend"), resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.stage", "dev"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "subscription_protocol", api.GraphQLSubscriptionProtocolWS), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "websocket_subprotocol", api.GraphQLWebsocketSubprotocolDefault), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "readme", readme), ), }, { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, updatedRoutingURL, subgraphSchema), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, updatedRoutingURL, subgraphSchema, readme), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", updatedRoutingURL), ), @@ -48,7 +53,7 @@ func TestAccSubgraphResource(t *testing.T) { RefreshState: true, }, { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema, readme), Destroy: true, }, }, @@ -62,26 +67,41 @@ func TestAccStandaloneSubgraphResource(t *testing.T) { federatedGraphRoutingURL := "https://federated-graph-standalone-subgraph-example.com" subgraphName := acctest.RandomWithPrefix("test-subgraph") - routingURL := "https://subgraph-standalone-example.com" + updatedRoutingURL := "https://updated-subgraph-standalone-example.com" subgraphSchema := acceptance.TestAccValidSubgraphSchema + readme := "Initial readme content" + updatedReadme := "Updated readme content" + resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema, readme), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", routingURL), resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.team", "backend"), resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.stage", "dev"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "readme", readme), ), }, { - Config: testStandaloneSubgraph(namespace, subgraphName, routingURL, subgraphSchema), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, updatedRoutingURL, subgraphSchema, updatedReadme), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", updatedRoutingURL), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.team", "backend"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.stage", "dev"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "readme", updatedReadme), + ), + }, + { + Config: testStandaloneSubgraph(namespace, subgraphName, routingURL, subgraphSchema, nil, nil, nil, nil, false), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), @@ -95,7 +115,7 @@ func TestAccStandaloneSubgraphResource(t *testing.T) { RefreshState: true, }, { - Config: testStandaloneSubgraph(namespace, subgraphName, routingURL, subgraphSchema), + Config: testStandaloneSubgraph(namespace, subgraphName, routingURL, subgraphSchema, nil, nil, nil, nil, false), Destroy: true, }, }, @@ -110,13 +130,14 @@ func TestAccSubgraphResourceInvalidSchema(t *testing.T) { federatedGraphName := acctest.RandomWithPrefix("test-subgraph") federatedGraphRoutingURL := "https://federated-graph-invalid-subgraph-schema-example.com" subgraphSchema := "invalid" + readme := "Initial readme content" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, subgraphRoutingURL, subgraphSchema), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, subgraphRoutingURL, subgraphSchema, readme), ExpectError: regexp.MustCompile(`.*ERR_INVALID_SUBGRAPH_SCHEMA*`), }, }, @@ -136,7 +157,7 @@ func TestAccStandaloneSubgraphResourcePublishSchema(t *testing.T) { ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema), + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, nil, nil, nil, nil, false), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), @@ -146,14 +167,75 @@ func TestAccStandaloneSubgraphResourcePublishSchema(t *testing.T) { ), }, { - Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, updatedSubgraphSchema), + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, updatedSubgraphSchema, nil, nil, nil, nil, false), ExpectError: regexp.MustCompile(`.*ERR_INVALID_SUBGRAPH_SCHEMA*`), }, }, }) } -func testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL, subgraphSchema string) string { +func TestOptionalValuesOfSubgraphResource(t *testing.T) { + namespace := acctest.RandomWithPrefix("test-namespace") + subgraphName := acctest.RandomWithPrefix("test-subgraph") + subgraphRoutingURL := "https://subgraph-publish-schema-example.com" + + subgraphSchema := acceptance.TestAccValidSubgraphSchema + + readme := "Initial readme content" + subgraphSubscriptionURL := "https://subgraph-publish-schema-example.com/ws" + subscriptionProtocol := api.GraphQLSubscriptionProtocolSSE + websocketSubprotocol := api.GraphQLWebsocketSubprotocolGraphQLWS + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, &readme, &subgraphSubscriptionURL, &subscriptionProtocol, &websocketSubprotocol, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", subgraphRoutingURL), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "readme", readme), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "subscription_url", subgraphSubscriptionURL), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "subscription_protocol", subscriptionProtocol), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "websocket_subprotocol", websocketSubprotocol), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "websocket_subprotocol", websocketSubprotocol), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.%", "0"), + ), + }, + { + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, &readme, &subgraphSubscriptionURL, &subscriptionProtocol, &websocketSubprotocol, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", subgraphRoutingURL), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "readme", readme), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "subscription_url", subgraphSubscriptionURL), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "subscription_protocol", subscriptionProtocol), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "websocket_subprotocol", websocketSubprotocol), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.team", "backend"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.stage", "dev"), + ), + }, + { + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema, nil, nil, nil, nil, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", subgraphRoutingURL), + resource.TestCheckNoResourceAttr("cosmo_subgraph.test", "readme"), + resource.TestCheckNoResourceAttr("cosmo_subgraph.test", "subscription_url"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.%", "0"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "subscription_protocol", api.GraphQLSubscriptionProtocolWS), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "websocket_subprotocol", api.GraphQLWebsocketSubprotocolDefault), + ), + }, + }, + }) +} + +func testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL, subgraphSchema, readme string) string { return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" @@ -179,11 +261,50 @@ resource "cosmo_subgraph" "test" { "team" = "backend", "stage" = "dev" } + readme = "%s" } -`, namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL, subgraphSchema) +`, namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL, subgraphSchema, readme) } -func testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema string) string { +func testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema string, readme, subscriptionUrl, subscriptionProtocol, websocketSubprotocol *string, unsetLabels bool) string { + var readmePart, subscriptionUrlPart, subscriptionProtocolPart, websocketSubprotocolPart string + if readme != nil { + readmePart = fmt.Sprintf(`readme = "%s"`, *readme) + } + + if subscriptionUrl != nil { + subscriptionUrlPart = fmt.Sprintf(`subscription_url = "%s"`, *subscriptionUrl) + } + + if subscriptionProtocol != nil { + subscriptionProtocolPart = fmt.Sprintf(`subscription_protocol = "%s"`, *subscriptionProtocol) + } + + if websocketSubprotocol != nil { + websocketSubprotocolPart = fmt.Sprintf(`websocket_subprotocol = "%s"`, *websocketSubprotocol) + } + + if unsetLabels { + return fmt.Sprintf(` +resource "cosmo_namespace" "test" { + name = "%s" +} + +resource "cosmo_subgraph" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + routing_url = "%s" + schema = <<-EOT + %s + EOT + %s + %s + %s + %s +} +`, namespace, subgraphName, subgraphRoutingURL, subgraphSchema, readmePart, subscriptionUrlPart, subscriptionProtocolPart, websocketSubprotocolPart) + } + return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" @@ -200,6 +321,10 @@ resource "cosmo_subgraph" "test" { "team" = "backend", "stage" = "dev" } + %s + %s + %s + %s } -`, namespace, subgraphName, subgraphRoutingURL, subgraphSchema) +`, namespace, subgraphName, subgraphRoutingURL, subgraphSchema, readmePart, subscriptionUrlPart, subscriptionProtocolPart, websocketSubprotocolPart) } diff --git a/internal/utils/helpers.go b/internal/utils/helpers.go index 9450270..e6aa740 100644 --- a/internal/utils/helpers.go +++ b/internal/utils/helpers.go @@ -21,3 +21,10 @@ func ConvertHeadersToStringList(headersList types.List) []string { } return headers } + +func GetValueOrDefault[T any](value *T, defaultValue T) T { + if value == nil { + return defaultValue + } + return *value +}