From f05636e22a36df15e55c6ecbf7dfe4217c40b50c Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Thu, 19 Sep 2024 20:35:25 +0200 Subject: [PATCH 01/27] feat: implement contract enhance error handling --- go.mod | 3 +- go.sum | 10 +- internal/api/contract.go | 70 ++++++ internal/api/errors.go | 53 ++++- internal/api/federated_graph.go | 47 ++-- internal/api/monograph.go | 51 ++--- internal/api/namespace.go | 49 +++-- internal/api/subgraph.go | 60 ++--- internal/api/token.go | 16 +- internal/provider/provider.go | 3 + .../contract/data_source_cosmo_contract.go | 140 ++++++++++++ .../data_source_cosmo_contract_test.go | 61 +++++ internal/service/contract/errors.go | 14 ++ .../contract/resource_cosmo_contract.go | 208 ++++++++++++++++++ .../contract/resource_cosmo_contract_test.go | 60 +++++ .../resource_cosmo_federated_graph.go | 12 +- .../resource_cosmo_federated_graph_test.go | 2 +- .../monograph/data_source_cosmo_monograph.go | 19 +- internal/service/monograph/errors.go | 1 + .../monograph/resource_cosmo_monograph.go | 61 +++-- .../data_source_cosmo_namespace_test.go | 3 +- .../namespace/resource_cosmo_namespace.go | 6 +- .../resource_cosmo_namespace_test.go | 5 +- internal/service/router-token/errors.go | 1 - .../router-token/resource_cosmo_token.go | 15 +- .../subgraph/data_source_cosmo_subgraph.go | 24 +- internal/service/subgraph/errors.go | 2 + .../subgraph/resource_cosmo_subgraph.go | 94 +++++--- 28 files changed, 891 insertions(+), 199 deletions(-) create mode 100644 internal/api/contract.go create mode 100644 internal/service/contract/data_source_cosmo_contract.go create mode 100644 internal/service/contract/data_source_cosmo_contract_test.go create mode 100644 internal/service/contract/errors.go create mode 100644 internal/service/contract/resource_cosmo_contract.go create mode 100644 internal/service/contract/resource_cosmo_contract_test.go diff --git a/go.mod b/go.mod index 7861a1f..a6d8d9d 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 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 ) @@ -16,7 +17,7 @@ require ( github.com/agext/levenshtein v1.2.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect - github.com/hashicorp/hcl/v2 v2.20.1 // indirect + github.com/hashicorp/hcl/v2 v2.21.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/go.sum b/go.sum index ff2732b..37819ab 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI= github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU= -github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= -github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= +github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= +github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= @@ -113,6 +113,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw= +github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -205,8 +207,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/api/contract.go b/internal/api/contract.go new file mode 100644 index 0000000..c06aa4e --- /dev/null +++ b/internal/api/contract.go @@ -0,0 +1,70 @@ +package api + +import ( + "context" + + "connectrpc.com/connect" + common "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" + 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, + }) + + response, err := p.Client.CreateContract(ctx, request) + if err != nil { + return nil, &ApiError{Err: err, Reason: "CreateContract", Status: common.EnumStatusCode_ERR} + } + + if response.Msg == nil { + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "CreateContract", Status: common.EnumStatusCode_ERR} + } + + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError + } + + 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, + }) + + response, err := p.Client.UpdateContract(ctx, request) + if err != nil { + return nil, &ApiError{Err: err, Reason: "UpdateContract", Status: common.EnumStatusCode_ERR} + } + + if response.Msg == nil { + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "UpdateContract", Status: common.EnumStatusCode_ERR} + } + + apiError := handleErrorCodes(response.Msg.GetResponse().Code, *response.Msg.GetResponse().Details) + if apiError != nil { + return nil, apiError + } + + return response.Msg, nil +} + +func (p *PlatformClient) DeleteContract(ctx context.Context, name, namespace string) *ApiError { + return p.DeleteFederatedGraph(ctx, name, namespace) +} + +func (p *PlatformClient) GetContract(ctx context.Context, name, namespace string) (*platformv1.GetFederatedGraphByNameResponse, *ApiError) { + return p.GetFederatedGraph(ctx, name, namespace) +} diff --git a/internal/api/errors.go b/internal/api/errors.go index ae7af37..fe6a093 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -1,34 +1,63 @@ package api import ( + "errors" "fmt" + "strings" - "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" + common "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" ) var ( - ErrEmptyMsg = fmt.Errorf("empty message") - ErrNotFound = fmt.Errorf("resource not found") - ErrSubgraphCompositionFailed = fmt.Errorf("subgraph composition failed") + ErrUnknown = errors.New("ErrUnknown") + ErrGeneral = errors.New("ErrGeneral") + ErrNotFound = errors.New("ErrNotFound") + ErrSubgraphCompositionFailed = errors.New("ErrSubgraphCompositionFailed") + ErrEmptyMsg = fmt.Errorf("ErrEmptyMsg") + ErrContractCompositionFailed = fmt.Errorf("ErrContractCompositionFailed") ) -func IsNotFoundError(err error) bool { - return err == ErrNotFound +const ( + ContractCompositionFailedReason = "A contract can only be created if its respective source graph has composed successfully" +) + +func IsNotFoundError(err *ApiError) bool { + return errors.Is(err.Err, ErrNotFound) +} + +func IsSubgraphCompositionFailedError(err *ApiError) bool { + return errors.Is(err.Err, ErrSubgraphCompositionFailed) +} + +func IsContractCompositionFailedError(err *ApiError) bool { + return errors.Is(err.Err, ErrContractCompositionFailed) +} + +type ApiError struct { + Err error + Reason string + Status common.EnumStatusCode } -func IsSubgraphCompositionFailedError(err error) bool { - return err == ErrSubgraphCompositionFailed +func (e *ApiError) Error() string { + return fmt.Sprintf("%s: %s (status: %s)", e.Err.Error(), e.Reason, e.Status.String()) } -func handleErrorCodes(statusCode common.EnumStatusCode) error { +func handleErrorCodes(statusCode common.EnumStatusCode, reason string) *ApiError { + if strings.Contains(reason, ContractCompositionFailedReason) { + return &ApiError{Err: ErrContractCompositionFailed, Reason: reason, Status: statusCode} + } + switch statusCode { case common.EnumStatusCode_OK: return nil case common.EnumStatusCode_ERR_SUBGRAPH_COMPOSITION_FAILED: - return ErrSubgraphCompositionFailed + return &ApiError{Err: ErrSubgraphCompositionFailed, Reason: reason, Status: statusCode} case common.EnumStatusCode_ERR_NOT_FOUND: - return ErrNotFound + return &ApiError{Err: ErrNotFound, Reason: reason, Status: statusCode} + case common.EnumStatusCode_ERR: + return &ApiError{Err: ErrGeneral, Reason: reason, Status: statusCode} default: - return fmt.Errorf("failed to create resource: %s", statusCode.String()) + return &ApiError{Err: ErrUnknown, Reason: reason, Status: statusCode} } } diff --git a/internal/api/federated_graph.go b/internal/api/federated_graph.go index 8a5ece5..fb054a2 100644 --- a/internal/api/federated_graph.go +++ b/internal/api/federated_graph.go @@ -4,10 +4,11 @@ import ( "context" "connectrpc.com/connect" + "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" ) -func (p *PlatformClient) CreateFederatedGraph(ctx context.Context, admissionWebhookSecret *string, graph *platformv1.FederatedGraph) (*platformv1.CreateFederatedGraphResponse, error) { +func (p *PlatformClient) CreateFederatedGraph(ctx context.Context, admissionWebhookSecret *string, graph *platformv1.FederatedGraph) (*platformv1.CreateFederatedGraphResponse, *ApiError) { var admissionWebhookURL string if graph.AdmissionWebhookUrl != nil { admissionWebhookURL = *graph.AdmissionWebhookUrl @@ -27,16 +28,16 @@ func (p *PlatformClient) CreateFederatedGraph(ctx context.Context, admissionWebh response, err := p.Client.CreateFederatedGraph(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "CreateFederatedGraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "CreateFederatedGraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return nil, err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg, nil @@ -60,22 +61,22 @@ func (p *PlatformClient) UpdateFederatedGraph(ctx context.Context, admissionWebh response, err := p.Client.UpdateFederatedGraph(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "UpdateFederatedGraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "UpdateFederatedGraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return nil, err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg, nil } -func (p *PlatformClient) DeleteFederatedGraph(ctx context.Context, name, namespace string) error { +func (p *PlatformClient) DeleteFederatedGraph(ctx context.Context, name, namespace string) *ApiError { request := connect.NewRequest(&platformv1.DeleteFederatedGraphRequest{ Name: name, Namespace: namespace, @@ -83,22 +84,22 @@ func (p *PlatformClient) DeleteFederatedGraph(ctx context.Context, name, namespa response, err := p.Client.DeleteFederatedGraph(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "DeleteFederatedGraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "DeleteFederatedGraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil } -func (p *PlatformClient) GetFederatedGraph(ctx context.Context, name, namespace string) (*platformv1.GetFederatedGraphByNameResponse, error) { +func (p *PlatformClient) GetFederatedGraph(ctx context.Context, name, namespace string) (*platformv1.GetFederatedGraphByNameResponse, *ApiError) { request := connect.NewRequest(&platformv1.GetFederatedGraphByNameRequest{ Name: name, Namespace: namespace, @@ -106,16 +107,16 @@ func (p *PlatformClient) GetFederatedGraph(ctx context.Context, name, namespace response, err := p.Client.GetFederatedGraphByName(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "GetFederatedGraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "GetFederatedGraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return nil, err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg, nil diff --git a/internal/api/monograph.go b/internal/api/monograph.go index 011ca7f..ea5d090 100644 --- a/internal/api/monograph.go +++ b/internal/api/monograph.go @@ -4,10 +4,11 @@ import ( "context" "connectrpc.com/connect" + "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" ) -func (p PlatformClient) CreateMonograph(ctx context.Context, name string, namespace string, routingUrl string, graphUrl string, subscriptionUrl *string, readme *string, websocketSubprotocol string, subscriptionProtocol string, admissionWebhookUrl string, admissionWebhookSecret string) error { +func (p PlatformClient) CreateMonograph(ctx context.Context, name string, namespace string, routingUrl string, graphUrl string, subscriptionUrl *string, readme *string, websocketSubprotocol string, subscriptionProtocol string, admissionWebhookUrl string, admissionWebhookSecret string) (*platformv1.CreateMonographResponse, *ApiError) { request := connect.NewRequest(&platformv1.CreateMonographRequest{ Name: name, Namespace: namespace, @@ -22,22 +23,22 @@ func (p PlatformClient) CreateMonograph(ctx context.Context, name string, namesp }) response, err := p.Client.CreateMonograph(ctx, request) if err != nil { - return err + return nil, &ApiError{Err: err, Reason: "CreateMonograph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "CreateMonograph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } - return nil + return response.Msg, nil } -func (p PlatformClient) UpdateMonograph(ctx context.Context, name string, namespace string, routingUrl string, graphUrl string, subscriptionUrl *string, readme *string, websocketSubprotocol string, subscriptionProtocol string, admissionWebhookUrl string, admissionWebhookSecret string) error { +func (p PlatformClient) UpdateMonograph(ctx context.Context, name string, namespace string, routingUrl string, graphUrl string, subscriptionUrl *string, readme *string, websocketSubprotocol string, subscriptionProtocol string, admissionWebhookUrl string, admissionWebhookSecret string) *ApiError { request := connect.NewRequest(&platformv1.UpdateMonographRequest{ Name: name, Namespace: namespace, @@ -52,60 +53,60 @@ func (p PlatformClient) UpdateMonograph(ctx context.Context, name string, namesp }) response, err := p.Client.UpdateMonograph(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "UpdateMonograph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "UpdateMonograph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil } -func (p PlatformClient) DeleteMonograph(ctx context.Context, name string, namespace string) error { +func (p PlatformClient) DeleteMonograph(ctx context.Context, name string, namespace string) *ApiError { request := connect.NewRequest(&platformv1.DeleteMonographRequest{ Name: name, Namespace: namespace, }) response, err := p.Client.DeleteMonograph(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "DeleteMonograph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "DeleteMonograph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil } -func (p PlatformClient) GetMonograph(ctx context.Context, name string, namespace string) (*platformv1.FederatedGraph, error) { +func (p PlatformClient) GetMonograph(ctx context.Context, name string, namespace string) (*platformv1.FederatedGraph, *ApiError) { request := connect.NewRequest(&platformv1.GetFederatedGraphByNameRequest{ Name: name, Namespace: namespace, }) response, err := p.Client.GetFederatedGraphByName(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "GetMonograph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "GetMonograph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return nil, err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg.Graph, nil diff --git a/internal/api/namespace.go b/internal/api/namespace.go index a2599e0..f6fc1e2 100644 --- a/internal/api/namespace.go +++ b/internal/api/namespace.go @@ -5,45 +5,46 @@ import ( "connectrpc.com/connect" + "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" ) -func (p PlatformClient) CreateNamespace(ctx context.Context, name string) error { +func (p PlatformClient) CreateNamespace(ctx context.Context, name string) *ApiError { request := connect.NewRequest(&platformv1.CreateNamespaceRequest{Name: name}) response, err := p.Client.CreateNamespace(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "CreateSubgraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "CreateNamespace", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } - return err + return nil } -func (p PlatformClient) RenameNamespace(ctx context.Context, oldName, newName string) error { +func (p PlatformClient) RenameNamespace(ctx context.Context, oldName, newName string) *ApiError { request := connect.NewRequest(&platformv1.RenameNamespaceRequest{ Name: oldName, NewName: newName, }) response, err := p.Client.RenameNamespace(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "RenameNamespace", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "RenameNamespace", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil @@ -53,35 +54,35 @@ func (p PlatformClient) DeleteNamespace(ctx context.Context, name string) error request := connect.NewRequest(&platformv1.DeleteNamespaceRequest{Name: name}) response, err := p.Client.DeleteNamespace(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "DeleteNamespace", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "DeleteNamespace", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil } -func (p PlatformClient) ListNamespaces(ctx context.Context) ([]*platformv1.Namespace, error) { +func (p PlatformClient) ListNamespaces(ctx context.Context) ([]*platformv1.Namespace, *ApiError) { request := connect.NewRequest(&platformv1.GetNamespacesRequest{}) response, err := p.Client.GetNamespaces(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "ListNamespaces", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "ListNamespaces", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return nil, err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg.Namespaces, nil diff --git a/internal/api/subgraph.go b/internal/api/subgraph.go index 43af1fe..cb8ee6b 100644 --- a/internal/api/subgraph.go +++ b/internal/api/subgraph.go @@ -4,10 +4,11 @@ import ( "context" "connectrpc.com/connect" + "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" 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) error { +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, @@ -23,22 +24,22 @@ func (p PlatformClient) CreateSubgraph(ctx context.Context, name string, namespa }) response, err := p.Client.CreateFederatedSubgraph(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "CreateSubgraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "CreateSubgraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } 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) error { +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, @@ -54,66 +55,66 @@ func (p PlatformClient) UpdateSubgraph(ctx context.Context, name, namespace, rou response, err := p.Client.UpdateSubgraph(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "UpdateSubgraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "UpdateSubgraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil } -func (p PlatformClient) DeleteSubgraph(ctx context.Context, name, namespace string) error { +func (p PlatformClient) DeleteSubgraph(ctx context.Context, name, namespace string) *ApiError { request := connect.NewRequest(&platformv1.DeleteFederatedSubgraphRequest{ SubgraphName: name, Namespace: namespace, }) response, err := p.Client.DeleteFederatedSubgraph(ctx, request) if err != nil { - return err + return &ApiError{Err: err, Reason: "DeleteSubgraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return ErrEmptyMsg + return &ApiError{Err: ErrEmptyMsg, Reason: "DeleteSubgraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return apiError } return nil } -func (p PlatformClient) GetSubgraph(ctx context.Context, name, namespace string) (*platformv1.Subgraph, error) { +func (p PlatformClient) GetSubgraph(ctx context.Context, name, namespace string) (*platformv1.Subgraph, *ApiError) { request := connect.NewRequest(&platformv1.GetSubgraphByNameRequest{ Name: name, Namespace: namespace, }) response, err := p.Client.GetSubgraphByName(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "GetSubgraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "GetSubgraph", Status: common.EnumStatusCode_ERR} } - err = handleErrorCodes(response.Msg.GetResponse().Code) - if err != nil { - return nil, err + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg.GetGraph(), nil } -func (p PlatformClient) PublishSubgraph(ctx context.Context, name, namespace, schema string) (*platformv1.PublishFederatedSubgraphResponse, error) { +func (p PlatformClient) PublishSubgraph(ctx context.Context, name, namespace, schema string) (*platformv1.PublishFederatedSubgraphResponse, *ApiError) { request := connect.NewRequest(&platformv1.PublishFederatedSubgraphRequest{ Name: name, Namespace: namespace, @@ -121,11 +122,16 @@ func (p PlatformClient) PublishSubgraph(ctx context.Context, name, namespace, sc }) response, err := p.Client.PublishFederatedSubgraph(ctx, request) if err != nil { - return nil, err + return nil, &ApiError{Err: err, Reason: "PublishSubgraph", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return nil, ErrEmptyMsg + return nil, &ApiError{Err: ErrEmptyMsg, Reason: "PublishSubgraph", Status: common.EnumStatusCode_ERR} + } + + apiError := handleErrorCodes(response.Msg.GetResponse().Code, response.Msg.String()) + if apiError != nil { + return nil, apiError } return response.Msg, nil diff --git a/internal/api/token.go b/internal/api/token.go index 40e7501..d8424c0 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -9,7 +9,7 @@ import ( platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" ) -func (p PlatformClient) CreateToken(ctx context.Context, name, graphName, namespace string) (string, error) { +func (p PlatformClient) CreateToken(ctx context.Context, name, graphName, namespace string) (string, *ApiError) { request := connect.NewRequest(&platformv1.CreateFederatedGraphTokenRequest{ GraphName: graphName, Namespace: namespace, @@ -18,21 +18,21 @@ func (p PlatformClient) CreateToken(ctx context.Context, name, graphName, namesp response, err := p.Client.CreateFederatedGraphToken(ctx, request) if err != nil { - return "", err + return "", &ApiError{Err: err, Reason: "CreateToken", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return "", fmt.Errorf("failed to create token, the server response is nil") + return "", &ApiError{Err: ErrEmptyMsg, Reason: "CreateToken", Status: common.EnumStatusCode_ERR} } if response.Msg.GetResponse().Code != common.EnumStatusCode_OK { - return "", fmt.Errorf("failed to create token: %s", response.Msg.GetResponse().GetDetails()) + return "", &ApiError{Err: fmt.Errorf("failed to create token: %s", response.Msg.GetResponse().GetDetails()), Reason: "CreateToken", Status: common.EnumStatusCode_ERR} } return response.Msg.Token, nil } -func (p PlatformClient) DeleteToken(ctx context.Context, tokenName, graphName, namespace string) error { +func (p PlatformClient) DeleteToken(ctx context.Context, tokenName, graphName, namespace string) *ApiError { request := connect.NewRequest(&platformv1.DeleteRouterTokenRequest{ TokenName: tokenName, Namespace: namespace, @@ -40,15 +40,15 @@ func (p PlatformClient) DeleteToken(ctx context.Context, tokenName, graphName, n response, err := p.Client.DeleteRouterToken(ctx, request) if err != nil { - return fmt.Errorf("failed to delete token: %w", err) + return &ApiError{Err: err, Reason: "DeleteToken", Status: common.EnumStatusCode_ERR} } if response.Msg == nil { - return fmt.Errorf("failed to delete token, the server response is nil") + return &ApiError{Err: ErrEmptyMsg, Reason: "DeleteToken", Status: common.EnumStatusCode_ERR} } if response.Msg.GetResponse().Code != common.EnumStatusCode_OK { - return fmt.Errorf("failed to delete token: %s", response.Msg) + return &ApiError{Err: fmt.Errorf("failed to delete token: %s", response.Msg), Reason: "DeleteToken", Status: common.EnumStatusCode_ERR} } return nil diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8e6e091..d784e62 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,6 +13,7 @@ import ( "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/api" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/utils" + contract "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/service/contract" federated_graph "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/service/federated-graph" monograph "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/service/monograph" namespace "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/service/namespace" @@ -98,6 +99,7 @@ func (p *CosmoProvider) Resources(ctx context.Context) []func() resource.Resourc subgraph.NewSubgraphResource, monograph.NewMonographResource, router_token.NewTokenResource, + contract.NewContractResource, } } @@ -107,6 +109,7 @@ func (p *CosmoProvider) DataSources(ctx context.Context) []func() datasource.Dat subgraph.NewSubgraphDataSource, namespace.NewNamespaceDataSource, monograph.NewMonographDataSource, + contract.NewContractDataSource, } } diff --git a/internal/service/contract/data_source_cosmo_contract.go b/internal/service/contract/data_source_cosmo_contract.go new file mode 100644 index 0000000..dd905d1 --- /dev/null +++ b/internal/service/contract/data_source_cosmo_contract.go @@ -0,0 +1,140 @@ +package contract + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/api" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/utils" +) + +func NewContractDataSource() datasource.DataSource { + return &contractDataSource{} +} + +type contractDataSource struct { + client *api.PlatformClient +} + +type contractDataSourceModel 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"` + AdmissionWebhookUrl types.String `tfsdk:"admission_webhook_url"` + AdmissionWebhookSecret types.String `tfsdk:"admission_webhook_secret"` + LabelMatchers types.Map `tfsdk:"label_matchers"` +} + +func (d *contractDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_contract" +} + +func (d *contractDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The unique identifier of the federated graph resource, automatically generated by the system.", + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the federated graph.", + Required: true, + }, + "namespace": schema.StringAttribute{ + MarkdownDescription: "The namespace in which the federated graph is located.", + Required: true, + }, + "readme": schema.StringAttribute{ + MarkdownDescription: "Readme content for the federated graph.", + Computed: true, + }, + "admission_webhook_url": schema.StringAttribute{ + MarkdownDescription: "The URL for the admission webhook that will be triggered during graph operations.", + Computed: true, + }, + "admission_webhook_secret": schema.StringAttribute{ + MarkdownDescription: "The secret token used to authenticate the admission webhook requests.", + Computed: true, + Sensitive: true, + }, + "label_matchers": schema.MapAttribute{ + MarkdownDescription: "A list of label matchers used to select the services that will form the federated graph.", + Computed: true, + ElementType: types.StringType, + }, + "routing_url": schema.StringAttribute{ + MarkdownDescription: "The URL for the federated graph.", + Computed: true, + }, + }, + } +} + +func (d *contractDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*api.PlatformClient) + if !ok { + utils.AddDiagnosticError(resp, + ErrUnexpectedDataSourceType, + "Expected *api.PlatformClient, got: %T. Please report this issue to the provider developers.", + ) + return + } + + d.client = client +} + +func (d *contractDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data contractDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if data.Name.IsNull() || data.Name.ValueString() == "" { + utils.AddDiagnosticError(resp, + ErrReadingContract, + "The 'name' attribute is required.", + ) + return + } + + namespace := data.Namespace.ValueString() + if namespace == "" { + namespace = "default" + } + + apiResponse, apiError := d.client.GetContract(ctx, data.Name.ValueString(), namespace) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrReadingContract, + "Could not read contract: "+apiError.Error(), + ) + return + } + + 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()) + + if graph.Readme != nil { + data.Readme = types.StringValue(*graph.Readme) + } + + tflog.Trace(ctx, "Read contract data source", map[string]interface{}{ + "id": data.Id.ValueString(), + }) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/service/contract/data_source_cosmo_contract_test.go b/internal/service/contract/data_source_cosmo_contract_test.go new file mode 100644 index 0000000..e8dacb3 --- /dev/null +++ b/internal/service/contract/data_source_cosmo_contract_test.go @@ -0,0 +1,61 @@ +package contract_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/acceptance" +) + +func TestAccContractDataSource(t *testing.T) { + name := acctest.RandomWithPrefix("test-contract") + namespace := acctest.RandomWithPrefix("test-namespace") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccContractDataSourceConfig(namespace, name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.cosmo_contract.test", "name", name), + resource.TestCheckResourceAttr("data.cosmo_contract.test", "namespace", namespace), + ), + }, + { + ResourceName: "data.cosmo_contract.test", + RefreshState: true, + }, + }, + }) +} + +func testAccContractDataSourceConfig(namespace, name string) string { + return fmt.Sprintf(` +resource "cosmo_namespace" "test" { + name = "%s" +} + +resource "cosmo_monograph" "source_graph" { + name = "source-graph" + namespace = cosmo_namespace.test.name + routing_url = "https://example.com" + graph_url = "https://example.com" +} + +resource "cosmo_contract" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + source = cosmo_monograph.source_graph.name + routing_url = "https://example.com" + readme = "Initial readme content" +} + +data "cosmo_contract" "test" { + name = cosmo_contract.test.name + namespace = cosmo_contract.test.namespace +} +`, namespace, name) +} diff --git a/internal/service/contract/errors.go b/internal/service/contract/errors.go new file mode 100644 index 0000000..d668338 --- /dev/null +++ b/internal/service/contract/errors.go @@ -0,0 +1,14 @@ +package contract + +const ( + ErrInvalidContractName = "Invalid Contract Name" + ErrCreatingContract = "Error Creating Contract" + ErrCompositionError = "Composition Error" + ErrRetrievingContract = "Error Retrieving Contract" + ErrInvalidResourceID = "Invalid Resource ID" + ErrReadingContract = "Error Reading Contract" + ErrUpdatingContract = "Error Updating Contract" + ErrDeletingContract = "Error Deleting Contract" + ErrUnexpectedDataSourceType = "Unexpected Data Source Configure Type" + ErrUnexpectedResourceType = "Unexpected Resource Configure Type" +) diff --git a/internal/service/contract/resource_cosmo_contract.go b/internal/service/contract/resource_cosmo_contract.go new file mode 100644 index 0000000..95de763 --- /dev/null +++ b/internal/service/contract/resource_cosmo_contract.go @@ -0,0 +1,208 @@ +package contract + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/api" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/utils" +) + +func NewContractResource() resource.Resource { + return &contractResource{} +} + +type contractResource struct { + client *api.PlatformClient +} + +type contractResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + SourceGraphName types.String `tfsdk:"source"` + Namespace types.String `tfsdk:"namespace"` + ExcludeTags types.List `tfsdk:"exclude_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"` +} + +func (r *contractResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_contract" +} + +func (r *contractResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "source": schema.StringAttribute{ + Required: true, + }, + "namespace": schema.StringAttribute{ + Required: true, + }, + "exclude_tags": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + "readme": schema.StringAttribute{ + Optional: true, + }, + "admission_webhook_url": schema.StringAttribute{ + Optional: true, + }, + "admission_webhook_secret": schema.StringAttribute{ + Optional: true, + }, + "routing_url": schema.StringAttribute{ + Required: true, + }, + }, + } +} + +func (r *contractResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*api.PlatformClient) + if !ok { + utils.AddDiagnosticError(resp, + ErrUnexpectedResourceType, + fmt.Sprintf("Expected *api.PlatformClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *contractResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data contractResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + 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()) + 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(), + ) + return + } + } + + data.ID = data.Name + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *contractResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data contractResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *contractResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data contractResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + excludeTags, err := utils.ConvertLabelMatchers(data.ExcludeTags) + if err != nil { + utils.AddDiagnosticError(resp, + ErrUpdatingContract, + "Could not update contract: "+err.Error(), + ) + return + } + + _, apiError := r.client.UpdateContract(ctx, data.Name.ValueString(), data.Namespace.ValueString(), excludeTags) + if apiError != nil { + if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrUpdatingContract, + "Contract composition failed: "+apiError.Error(), + ) + } else { + utils.AddDiagnosticError(resp, + ErrUpdatingContract, + "Could not update contract: "+apiError.Error(), + ) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *contractResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data contractResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + apiError := r.client.DeleteContract(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrDeletingContract, + "Contract composition failed: "+apiError.Error(), + ) + } else if api.IsNotFoundError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrDeletingContract, + "Contract composition failed: "+apiError.Error(), + ) + resp.State.RemoveResource(ctx) + } else { + utils.AddDiagnosticError(resp, + ErrDeletingContract, + "Could not delete contract: "+apiError.Error(), + ) + return + } + } +} + +func (r *contractResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/service/contract/resource_cosmo_contract_test.go b/internal/service/contract/resource_cosmo_contract_test.go new file mode 100644 index 0000000..2e17db2 --- /dev/null +++ b/internal/service/contract/resource_cosmo_contract_test.go @@ -0,0 +1,60 @@ +package contract_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/acceptance" +) + +func TestAccContractResource(t *testing.T) { + name := acctest.RandomWithPrefix("test-contract") + namespace := acctest.RandomWithPrefix("test-namespace") + + readme := "Initial readme content" + + federatedGraphName := acctest.RandomWithPrefix("test-federated-graph") + federatedGraphroutingURL := "https://example.com:3000" + + graphUrl := "http://example.com/graphql" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccContractResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, graphUrl, name, 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), + ), + }, + }, + }) +} + +func testAccContractResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, graphUrl, contractName, contractReadme string) string { + return fmt.Sprintf(` +resource "cosmo_namespace" "test" { + name = "%s" +} + +resource "cosmo_monograph" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + routing_url = "%s" + graph_url = "%s" +} + +resource "cosmo_contract" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + source = cosmo_monograph.test.name + routing_url = "http://localhost:3003" + readme = "%s" +} +`, namespace, federatedGraphName, federatedGraphroutingURL, graphUrl, contractName, contractReadme) +} diff --git a/internal/service/federated-graph/resource_cosmo_federated_graph.go b/internal/service/federated-graph/resource_cosmo_federated_graph.go index 4e6298a..38f47c7 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph.go @@ -305,14 +305,14 @@ func (r *FederatedGraphResource) createFederatedGraph(ctx context.Context, data "label_matchers": labelMatchers, }) - _, err = r.client.CreateFederatedGraph(ctx, admissionWebhookSecret, &apiGraph) - if err != nil { - return nil, fmt.Errorf("could not create federated graph: %w", err) + _, apiError := r.client.CreateFederatedGraph(ctx, admissionWebhookSecret, &apiGraph) + if apiError != nil { + return nil, fmt.Errorf("could not create federated graph: %w", apiError) } - response, err := r.client.GetFederatedGraph(ctx, apiGraph.Name, apiGraph.Namespace) - if err != nil { - return nil, fmt.Errorf("could not retrieve federated graph: %w", err) + response, apiError := r.client.GetFederatedGraph(ctx, apiGraph.Name, apiGraph.Namespace) + if apiError != nil { + return nil, fmt.Errorf("could not retrieve federated graph: %w", apiError) } utils.DebugAction(ctx, DebugCreate, data.Name.ValueString(), data.Namespace.ValueString(), map[string]interface{}{ 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 0fcfe1a..32750c3 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph_test.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph_test.go @@ -59,7 +59,7 @@ func TestAccFederatedGraphResourceInvalidConfig(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccFederatedGraphResourceConfig(name, namespace, "invalid-url", ""), - ExpectError: regexp.MustCompile(`.*failed to create resource*`), + ExpectError: regexp.MustCompile(`.*Routing URL is not a valid URL*`), }, }, }) diff --git a/internal/service/monograph/data_source_cosmo_monograph.go b/internal/service/monograph/data_source_cosmo_monograph.go index 409a919..022cb41 100644 --- a/internal/service/monograph/data_source_cosmo_monograph.go +++ b/internal/service/monograph/data_source_cosmo_monograph.go @@ -102,7 +102,10 @@ func (d *MonographDataSource) Configure(ctx context.Context, req datasource.Conf client, ok := req.ProviderData.(*api.PlatformClient) if !ok { - utils.AddDiagnosticError(resp, ErrUnexpectedDataSourceType, fmt.Sprintf("Expected *client.PlatformClient, got: %T. Please report this issue to the provider developers.", req.ProviderData)) + utils.AddDiagnosticError(resp, + ErrUnexpectedDataSourceType, + fmt.Sprintf("Expected *api.PlatformClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) return } @@ -119,7 +122,10 @@ func (d *MonographDataSource) Read(ctx context.Context, req datasource.ReadReque } if data.Name.IsNull() || data.Name.ValueString() == "" { - utils.AddDiagnosticError(resp, ErrInvalidMonographName, fmt.Sprintf("The 'name' attribute is required for monograph in namespace: %s", data.Namespace.ValueString())) + utils.AddDiagnosticError(resp, + ErrInvalidMonographName, + "The 'name' attribute is required.", + ) return } @@ -128,9 +134,12 @@ func (d *MonographDataSource) Read(ctx context.Context, req datasource.ReadReque namespace = "default" } - monograph, err := d.client.GetMonograph(ctx, data.Name.ValueString(), namespace) - if err != nil { - utils.AddDiagnosticError(resp, ErrReadingMonograph, fmt.Sprintf("Could not read monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), namespace)) + monograph, apiError := d.client.GetMonograph(ctx, data.Name.ValueString(), namespace) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrReadingMonograph, + "Could not read monograph: "+apiError.Error(), + ) return } diff --git a/internal/service/monograph/errors.go b/internal/service/monograph/errors.go index 864eb23..60e30bf 100644 --- a/internal/service/monograph/errors.go +++ b/internal/service/monograph/errors.go @@ -10,4 +10,5 @@ const ( ErrUpdatingMonograph = "Error Updating Monograph" ErrDeletingMonograph = "Error Deleting Monograph" ErrUnexpectedDataSourceType = "Unexpected Data Source Configure Type" + ErrUnexpectedResourceType = "Unexpected Resource Configure Type" ) diff --git a/internal/service/monograph/resource_cosmo_monograph.go b/internal/service/monograph/resource_cosmo_monograph.go index e8dd0a3..8ee6f9e 100644 --- a/internal/service/monograph/resource_cosmo_monograph.go +++ b/internal/service/monograph/resource_cosmo_monograph.go @@ -120,7 +120,10 @@ func (r *MonographResource) Configure(ctx context.Context, req resource.Configur client, ok := req.ProviderData.(*api.PlatformClient) if !ok { - utils.AddDiagnosticError(resp, ErrUnexpectedDataSourceType, fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData)) + utils.AddDiagnosticError(resp, + ErrUnexpectedResourceType, + fmt.Sprintf("Expected *api.PlatformClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) return } @@ -136,11 +139,14 @@ func (r *MonographResource) Create(ctx context.Context, req resource.CreateReque } if data.Name.IsNull() || data.Name.ValueString() == "" { - utils.AddDiagnosticError(resp, ErrInvalidMonographName, fmt.Sprintf("The 'name' attribute is required for monograph in namespace: %s", data.Namespace.ValueString())) + utils.AddDiagnosticError(resp, + ErrInvalidMonographName, + "The 'name' attribute is required.", + ) return } - err := r.client.CreateMonograph( + _, err := r.client.CreateMonograph( ctx, data.Name.ValueString(), data.Namespace.ValueString(), @@ -154,13 +160,19 @@ func (r *MonographResource) Create(ctx context.Context, req resource.CreateReque data.AdmissionWebhookSecret.ValueString(), ) if err != nil { - utils.AddDiagnosticError(resp, ErrCreatingMonograph, fmt.Sprintf("Could not create monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), data.Namespace.ValueString())) + utils.AddDiagnosticError(resp, + ErrCreatingMonograph, + "Could not create monograph: "+err.Error(), + ) return } - monograph, err := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrRetrievingMonograph, fmt.Sprintf("Could not retrieve monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), data.Namespace.ValueString())) + monograph, apiError := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrRetrievingMonograph, + "Could not create monograph: "+apiError.Error(), + ) return } @@ -182,14 +194,20 @@ func (r *MonographResource) Read(ctx context.Context, req resource.ReadRequest, return } - monograph, err := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if err != nil { - if api.IsNotFoundError(err) { - utils.AddDiagnosticWarning(resp, "Monograph not found", fmt.Sprintf("Monograph '%s' not found will be recreated", data.Name.ValueString())) + monograph, apiError := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticWarning(resp, + "Monograph not found", + "Monograph not found will be recreated: "+apiError.Error(), + ) resp.State.RemoveResource(ctx) return } - utils.AddDiagnosticError(resp, ErrReadingMonograph, fmt.Sprintf("Could not read monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), data.Namespace.ValueString())) + utils.AddDiagnosticError(resp, + ErrRetrievingMonograph, + "Could not read monograph: "+apiError.Error(), + ) return } @@ -229,13 +247,19 @@ func (r *MonographResource) Update(ctx context.Context, req resource.UpdateReque data.AdmissionWebhookSecret.ValueString(), ) if err != nil { - utils.AddDiagnosticError(resp, ErrUpdatingMonograph, fmt.Sprintf("Could not update monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), data.Namespace.ValueString())) + utils.AddDiagnosticError(resp, + ErrUpdatingMonograph, + "Could not update monograph: "+err.Error(), + ) return } monograph, err := r.client.GetMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if err != nil { - utils.AddDiagnosticError(resp, ErrRetrievingMonograph, fmt.Sprintf("Could not fetch updated monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), data.Namespace.ValueString())) + utils.AddDiagnosticError(resp, + ErrRetrievingMonograph, + "Could not fetch updated monograph: "+err.Error(), + ) return } @@ -255,9 +279,12 @@ func (r *MonographResource) Delete(ctx context.Context, req resource.DeleteReque return } - err := r.client.DeleteMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrDeletingMonograph, fmt.Sprintf("Could not delete monograph: %s, name: %s, namespace: %s", err, data.Name.ValueString(), data.Namespace.ValueString())) + apiError := r.client.DeleteMonograph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrDeletingMonograph, + "Could not delete monograph: "+apiError.Error(), + ) return } diff --git a/internal/service/namespace/data_source_cosmo_namespace_test.go b/internal/service/namespace/data_source_cosmo_namespace_test.go index 2342272..343c7f6 100644 --- a/internal/service/namespace/data_source_cosmo_namespace_test.go +++ b/internal/service/namespace/data_source_cosmo_namespace_test.go @@ -5,11 +5,12 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/acceptance" ) func TestAccNamespaceDataSource(t *testing.T) { - rName := "test-namespace-unique" // Ensure a unique name + rName := acctest.RandomWithPrefix("test-namespace") resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, diff --git a/internal/service/namespace/resource_cosmo_namespace.go b/internal/service/namespace/resource_cosmo_namespace.go index f0791bc..2e795f1 100644 --- a/internal/service/namespace/resource_cosmo_namespace.go +++ b/internal/service/namespace/resource_cosmo_namespace.go @@ -77,9 +77,9 @@ func (r *NamespaceResource) Create(ctx context.Context, req resource.CreateReque return } - err := r.client.CreateNamespace(ctx, data.Name.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrCreatingNamespace, fmt.Sprintf("Could not create namespace: %s, name: %s", err, data.Name.ValueString())) + apiError := r.client.CreateNamespace(ctx, data.Name.ValueString()) + if apiError != nil { + utils.AddDiagnosticError(resp, ErrCreatingNamespace, fmt.Sprintf("Could not create namespace: %s, name: %s", apiError, data.Name.ValueString())) return } diff --git a/internal/service/namespace/resource_cosmo_namespace_test.go b/internal/service/namespace/resource_cosmo_namespace_test.go index 71e79f6..953fd78 100644 --- a/internal/service/namespace/resource_cosmo_namespace_test.go +++ b/internal/service/namespace/resource_cosmo_namespace_test.go @@ -6,12 +6,13 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/acceptance" ) func TestAccNamespaceResource(t *testing.T) { - rName := "test-namespace" - updatedName := "updated-namespace" + rName := acctest.RandomWithPrefix("test-namespace") + updatedName := acctest.RandomWithPrefix("updated-namespace") resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, diff --git a/internal/service/router-token/errors.go b/internal/service/router-token/errors.go index cb11e56..23cebc2 100644 --- a/internal/service/router-token/errors.go +++ b/internal/service/router-token/errors.go @@ -3,5 +3,4 @@ package router_token const ( ErrCreatingToken = "error creating token" ErrUnexpectedDataSourceType = "unexpected data source type" - ErrDeletingToken = "error deleting token" ) diff --git a/internal/service/router-token/resource_cosmo_token.go b/internal/service/router-token/resource_cosmo_token.go index ac8d840..83cf4cb 100644 --- a/internal/service/router-token/resource_cosmo_token.go +++ b/internal/service/router-token/resource_cosmo_token.go @@ -85,7 +85,7 @@ func (r *TokenResource) Configure(ctx context.Context, req resource.ConfigureReq client, ok := req.ProviderData.(*api.PlatformClient) if !ok { - utils.AddDiagnosticError(resp, ErrUnexpectedDataSourceType, fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData)) + utils.AddDiagnosticError(resp, ErrUnexpectedDataSourceType, fmt.Sprintf("Expected *api.PlatformClient, got: %T. Please report this issue to the provider developers.", req.ProviderData)) return } @@ -100,14 +100,17 @@ func (r *TokenResource) Create(ctx context.Context, req resource.CreateRequest, return } - apiResponse, err := r.client.CreateToken(ctx, data.Name.ValueString(), data.GraphName.ValueString(), data.Namespace.ValueString()) - if err != nil { - if api.IsNotFoundError(err) { - utils.AddDiagnosticWarning(resp, "Token not found", fmt.Sprintf("Token '%s' not found will be recreated", data.Name.ValueString())) + apiResponse, apiError := r.client.CreateToken(ctx, data.Name.ValueString(), data.GraphName.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + if api.IsNotFoundError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrCreatingToken, + fmt.Sprintf("Token '%s' not found will be recreated\nReason: %s", data.Name.ValueString(), apiError.Error()), + ) resp.State.RemoveResource(ctx) return } - utils.AddDiagnosticError(resp, ErrCreatingToken, fmt.Sprintf("Could not create token: %s", err)) + utils.AddDiagnosticError(resp, ErrCreatingToken, "Could not create token: "+apiError.Error()) return } diff --git a/internal/service/subgraph/data_source_cosmo_subgraph.go b/internal/service/subgraph/data_source_cosmo_subgraph.go index 5c9ee93..9f2d551 100644 --- a/internal/service/subgraph/data_source_cosmo_subgraph.go +++ b/internal/service/subgraph/data_source_cosmo_subgraph.go @@ -117,7 +117,10 @@ func (d *SubgraphDataSource) Configure(ctx context.Context, req datasource.Confi client, ok := req.ProviderData.(*api.PlatformClient) if !ok { - utils.AddDiagnosticError(resp, ErrUnexpectedDataSourceType, fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData)) + utils.AddDiagnosticError(resp, + ErrUnexpectedDataSourceType, + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) return } @@ -133,17 +136,26 @@ func (d *SubgraphDataSource) Read(ctx context.Context, req datasource.ReadReques } if data.Name.IsNull() || data.Name.ValueString() == "" { - utils.AddDiagnosticError(resp, ErrInvalidSubgraphName, "The 'name' attribute is required.") + utils.AddDiagnosticError(resp, + ErrInvalidSubgraphName, + "The 'name' attribute is required.", + ) return } if data.Namespace.IsNull() || data.Namespace.ValueString() == "" { - utils.AddDiagnosticError(resp, ErrInvalidNamespace, "The 'namespace' attribute is required.") + utils.AddDiagnosticError(resp, + ErrInvalidNamespace, + "The 'namespace' attribute is required.", + ) return } - subgraph, err := d.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrRetrievingSubgraph, fmt.Sprintf("Could not read subgraph '%s' in namespace '%s': %s", data.Name.ValueString(), data.Namespace.ValueString(), err)) + subgraph, apiError := d.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiError != nil { + utils.AddDiagnosticError(resp, + ErrRetrievingSubgraph, + fmt.Sprintf("Could not read subgraph '%s' in namespace '%s': %s", data.Name.ValueString(), data.Namespace.ValueString(), apiError.Error()), + ) return } diff --git a/internal/service/subgraph/errors.go b/internal/service/subgraph/errors.go index 986665e..bbaa3c7 100644 --- a/internal/service/subgraph/errors.go +++ b/internal/service/subgraph/errors.go @@ -8,5 +8,7 @@ const ( ErrDeletingSubgraph = "Error Deleting Subgraph" ErrPublishingSubgraph = "Error Publishing Subgraph" ErrUnexpectedDataSourceType = "Unexpected Data Source Configure Type" + ErrSubgraphNotFound = "Subgraph Not Found" + ErrSubgraphSchemaChanged = "Subgraph Schema Changed" ErrInvalidNamespace = "Invalid Namespace" ) diff --git a/internal/service/subgraph/resource_cosmo_subgraph.go b/internal/service/subgraph/resource_cosmo_subgraph.go index e5fcec6..225ecde 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph.go +++ b/internal/service/subgraph/resource_cosmo_subgraph.go @@ -53,7 +53,10 @@ func (r *SubgraphResource) Configure(ctx context.Context, req resource.Configure client, ok := req.ProviderData.(*api.PlatformClient) if !ok { - utils.AddDiagnosticError(resp, ErrUnexpectedDataSourceType, fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData)) + utils.AddDiagnosticError(resp, + ErrUnexpectedDataSourceType, + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) return } @@ -181,14 +184,17 @@ func (r *SubgraphResource) Read(ctx context.Context, req resource.ReadRequest, r return } - subgraph, err := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if err != nil { - if api.IsNotFoundError(err) { - utils.AddDiagnosticWarning(resp, "Subgraph not found", fmt.Sprintf("Subgraph '%s' not found will be recreated", data.Name.ValueString())) + subgraph, apiError := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + 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(), err)) + utils.AddDiagnosticError(resp, ErrRetrievingSubgraph, fmt.Sprintf("Could not fetch subgraph '%s': %s", data.Name.ValueString(), apiError.Error())) return } @@ -223,11 +229,17 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques if data.Schema.ValueString() != "" { apiResponse, err := r.client.PublishSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) if err != nil { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), err)) + utils.AddDiagnosticError(resp, + ErrPublishingSubgraph, + fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), err.Error()), + ) return } if apiResponse.HasChanged != nil && *apiResponse.HasChanged { - resp.Diagnostics.AddWarning("Subgraph schema has changed", fmt.Sprintf("The schema for subgraph '%s' has changed and was published.", data.Name.ValueString())) + resp.Diagnostics.AddWarning( + ErrSubgraphSchemaChanged, + fmt.Sprintf("The schema for subgraph '%s' has changed and was published.", data.Name.ValueString()), + ) } } @@ -238,19 +250,28 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques // TBD: This is only used in the update subgraph method and not used atm // headers := utils.ConvertHeadersToStringList(data.Headers) - err := 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()) - if err != nil { - if api.IsSubgraphCompositionFailedError(err) { - utils.AddDiagnosticWarning(resp, ErrUpdatingSubgraph, fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), err)) + 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()) + if apiErr != nil { + if api.IsSubgraphCompositionFailedError(apiErr) { + utils.AddDiagnosticWarning(resp, + ErrUpdatingSubgraph, + fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) } else { - utils.AddDiagnosticError(resp, ErrUpdatingSubgraph, fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), err)) + utils.AddDiagnosticError(resp, + ErrUpdatingSubgraph, + fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) return } } subgraph, err := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if err != nil { - utils.AddDiagnosticError(resp, ErrRetrievingSubgraph, fmt.Sprintf("Could not fetch updated subgraph '%s': %s", data.Name.ValueString(), err)) + utils.AddDiagnosticError(resp, + ErrRetrievingSubgraph, + fmt.Sprintf("Could not fetch updated subgraph '%s': %s", data.Name.ValueString(), err.Error()), + ) return } @@ -274,7 +295,10 @@ func (r *SubgraphResource) Delete(ctx context.Context, req resource.DeleteReques err := r.client.DeleteSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if err != nil { - utils.AddDiagnosticError(resp, ErrDeletingSubgraph, fmt.Sprintf("Could not delete subgraph '%s': %s", data.Name.ValueString(), err)) + utils.AddDiagnosticError(resp, + ErrDeletingSubgraph, + fmt.Sprintf("Could not delete subgraph '%s': %s", data.Name.ValueString(), err.Error()), + ) return } @@ -297,23 +321,39 @@ func (r *SubgraphResource) createAndPublishSubgraph(ctx context.Context, data Su } utils.DebugAction(ctx, "Labels", data.Name.ValueString(), data.Namespace.ValueString(), map[string]interface{}{"labels": labels}) - err := 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()) - if err != nil { - utils.AddDiagnosticError(resp, ErrCreatingSubgraph, fmt.Sprintf("Could not create subgraph '%s': %s", data.Name.ValueString(), err)) - return nil, err + 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()) + if apiErr != nil { + utils.AddDiagnosticError(resp, + ErrCreatingSubgraph, + fmt.Sprintf("Could not create subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) + return nil, apiErr } - subgraph, err := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrRetrievingSubgraph, fmt.Sprintf("Could not fetch created subgraph '%s': %s", data.Name.ValueString(), err)) - return nil, err + subgraph, apiErr := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) + if apiErr != nil { + utils.AddDiagnosticError(resp, + ErrRetrievingSubgraph, + fmt.Sprintf("Could not fetch created subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) + return nil, apiErr } if data.Schema.ValueString() != "" { - _, err := r.client.PublishSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrPublishingSubgraph, fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), err)) - return nil, err + _, apiErr := r.client.PublishSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) + if apiErr != nil { + if api.IsSubgraphCompositionFailedError(apiErr) { + utils.AddDiagnosticWarning(resp, + ErrPublishingSubgraph, + fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) + } else { + utils.AddDiagnosticError(resp, + ErrPublishingSubgraph, + fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) + return nil, apiErr + } } } From 5bf17fb9b9350974c0bf060250f71f40706c0e91 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Thu, 19 Sep 2024 20:35:59 +0200 Subject: [PATCH 02/27] chore: update docs and examples --- docs/data-sources/contract.md | 37 ++++++++++++++++ docs/data-sources/federated_graph.md | 9 ---- docs/data-sources/namespace.md | 9 ---- docs/data-sources/subgraph.md | 9 ---- docs/index.md | 9 ---- docs/resources/contract.md | 43 +++++++++++++++++++ docs/resources/federated_graph.md | 9 ---- docs/resources/namespace.md | 9 ---- docs/resources/router_token.md | 9 ---- docs/resources/subgraph.md | 9 ---- examples/cosmo-local/variables.tf | 2 +- .../cosmo_contract/data-source.tf | 4 ++ .../data-sources/cosmo_contract/provider.tf | 9 ++++ .../data-sources/cosmo_contract/variables.tf | 9 ++++ .../cosmo_federated_graph/data-source.tf | 9 ---- .../cosmo_federated_graph/provider.tf | 9 ++++ .../data-sources/cosmo_monograph/provider.tf | 9 ++++ .../cosmo_namespace/data-source.tf | 9 ---- .../data-sources/cosmo_namespace/provider.tf | 9 ++++ .../cosmo_subgraph/data-source.tf | 9 ---- .../data-sources/cosmo_subgraph/provider.tf | 9 ++++ examples/provider/_provider.tf | 9 ++++ examples/provider/main.tf | 9 ++++ examples/provider/provider.tf | 9 ---- examples/resources/cosmo_contract/outputs.tf | 15 +++++++ examples/resources/cosmo_contract/provider.tf | 9 ++++ examples/resources/cosmo_contract/resource.tf | 6 +++ .../resources/cosmo_contract/variables.tf | 15 +++++++ .../cosmo_federated_graph/provider.tf | 9 ++++ .../cosmo_federated_graph/resource.tf | 9 ---- .../resources/cosmo_namespace/provider.tf | 9 ++++ .../resources/cosmo_namespace/resource.tf | 9 ---- .../resources/cosmo_router_token/provider.tf | 9 ++++ .../resources/cosmo_router_token/resource.tf | 9 ---- examples/resources/cosmo_subgraph/provider.tf | 9 ++++ examples/resources/cosmo_subgraph/resource.tf | 9 ---- 36 files changed, 238 insertions(+), 145 deletions(-) create mode 100644 docs/data-sources/contract.md create mode 100644 docs/resources/contract.md create mode 100644 examples/data-sources/cosmo_contract/data-source.tf create mode 100644 examples/data-sources/cosmo_contract/provider.tf create mode 100644 examples/data-sources/cosmo_contract/variables.tf create mode 100644 examples/data-sources/cosmo_federated_graph/provider.tf create mode 100644 examples/data-sources/cosmo_monograph/provider.tf create mode 100644 examples/data-sources/cosmo_namespace/provider.tf create mode 100644 examples/data-sources/cosmo_subgraph/provider.tf create mode 100644 examples/provider/_provider.tf create mode 100644 examples/resources/cosmo_contract/outputs.tf create mode 100644 examples/resources/cosmo_contract/provider.tf create mode 100644 examples/resources/cosmo_contract/resource.tf create mode 100644 examples/resources/cosmo_contract/variables.tf create mode 100644 examples/resources/cosmo_federated_graph/provider.tf create mode 100644 examples/resources/cosmo_namespace/provider.tf create mode 100644 examples/resources/cosmo_router_token/provider.tf create mode 100644 examples/resources/cosmo_subgraph/provider.tf diff --git a/docs/data-sources/contract.md b/docs/data-sources/contract.md new file mode 100644 index 0000000..7f1921c --- /dev/null +++ b/docs/data-sources/contract.md @@ -0,0 +1,37 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cosmo_contract Data Source - cosmo" +subcategory: "" +description: |- + +--- + +# cosmo_contract (Data Source) + + + +## Example Usage + +```terraform +data "cosmo_subgraph" "test" { + name = var.name + namespace = var.namespace +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the federated graph. +- `namespace` (String) The namespace in which the federated graph is located. + +### Read-Only + +- `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. +- `id` (String) The unique identifier of the federated graph resource, automatically generated by the system. +- `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. diff --git a/docs/data-sources/federated_graph.md b/docs/data-sources/federated_graph.md index aadda67..fdc284d 100644 --- a/docs/data-sources/federated_graph.md +++ b/docs/data-sources/federated_graph.md @@ -13,15 +13,6 @@ Cosmo Federated Graph Data Source ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_federated_graph" "test" { name = var.name namespace = var.namespace diff --git a/docs/data-sources/namespace.md b/docs/data-sources/namespace.md index 164967e..71a0f08 100644 --- a/docs/data-sources/namespace.md +++ b/docs/data-sources/namespace.md @@ -13,15 +13,6 @@ Cosmo Namespace Data Source ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_namespace" "test" { name = var.name } diff --git a/docs/data-sources/subgraph.md b/docs/data-sources/subgraph.md index 2d848e1..25ab1b1 100644 --- a/docs/data-sources/subgraph.md +++ b/docs/data-sources/subgraph.md @@ -13,15 +13,6 @@ Cosmo Subgraph Data Source ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_subgraph" "test" { name = var.name namespace = var.namespace diff --git a/docs/index.md b/docs/index.md index 39902a8..9f15736 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,15 +20,6 @@ Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - provider "cosmo" { api_url = var.api_url api_key = var.api_key diff --git a/docs/resources/contract.md b/docs/resources/contract.md new file mode 100644 index 0000000..7dd80b0 --- /dev/null +++ b/docs/resources/contract.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cosmo_contract Resource - cosmo" +subcategory: "" +description: |- + +--- + +# cosmo_contract (Resource) + + + +## Example Usage + +```terraform +resource "cosmo_contract" "test" { + name = var.name + namespace = var.namespace + source = var.source_graph_name + routing_url = var.routing_url +} +``` + + +## Schema + +### Required + +- `name` (String) +- `namespace` (String) +- `routing_url` (String) +- `source` (String) + +### Optional + +- `admission_webhook_secret` (String) +- `admission_webhook_url` (String) +- `exclude_tags` (List of String) +- `readme` (String) + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/federated_graph.md b/docs/resources/federated_graph.md index 215651c..490cd40 100644 --- a/docs/resources/federated_graph.md +++ b/docs/resources/federated_graph.md @@ -16,15 +16,6 @@ For more information on federated graphs, please refer to the [Cosmo Documentati ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_federated_graph" "test" { name = var.name routing_url = var.routing_url diff --git a/docs/resources/namespace.md b/docs/resources/namespace.md index 420a257..375a72f 100644 --- a/docs/resources/namespace.md +++ b/docs/resources/namespace.md @@ -16,15 +16,6 @@ For more information on namespaces, please refer to the [Cosmo Documentation](ht ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_namespace" "test" { name = var.name } diff --git a/docs/resources/router_token.md b/docs/resources/router_token.md index 92da38d..61ca11b 100644 --- a/docs/resources/router_token.md +++ b/docs/resources/router_token.md @@ -16,15 +16,6 @@ For more information on router tokens, please refer to the [Cosmo Documentation] ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_router_token" "test" { name = var.name graph_name = var.graph_name diff --git a/docs/resources/subgraph.md b/docs/resources/subgraph.md index 33b1955..c9d1407 100644 --- a/docs/resources/subgraph.md +++ b/docs/resources/subgraph.md @@ -16,15 +16,6 @@ For more information on subgraphs, please refer to the [Cosmo Documentation](htt ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_subgraph" "test" { name = var.name namespace = var.namespace diff --git a/examples/cosmo-local/variables.tf b/examples/cosmo-local/variables.tf index 6ac179d..9a9f128 100644 --- a/examples/cosmo-local/variables.tf +++ b/examples/cosmo-local/variables.tf @@ -51,7 +51,7 @@ variable "cosmo" { release_name = "cosmo" chart = { name = "cosmo" - version = "0.11.1" + version = "0.11.2" namespace = "cosmo" repository = "oci://ghcr.io/wundergraph/cosmo/helm-charts" values = [] diff --git a/examples/data-sources/cosmo_contract/data-source.tf b/examples/data-sources/cosmo_contract/data-source.tf new file mode 100644 index 0000000..3298fa5 --- /dev/null +++ b/examples/data-sources/cosmo_contract/data-source.tf @@ -0,0 +1,4 @@ +data "cosmo_subgraph" "test" { + name = var.name + namespace = var.namespace +} \ No newline at end of file diff --git a/examples/data-sources/cosmo_contract/provider.tf b/examples/data-sources/cosmo_contract/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/data-sources/cosmo_contract/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/data-sources/cosmo_contract/variables.tf b/examples/data-sources/cosmo_contract/variables.tf new file mode 100644 index 0000000..52f3c98 --- /dev/null +++ b/examples/data-sources/cosmo_contract/variables.tf @@ -0,0 +1,9 @@ +variable "name" { + type = string + description = "The name of the subgraph to retrieve" +} + +variable "namespace" { + type = string + description = "The namespace of the subgraph" +} \ No newline at end of file diff --git a/examples/data-sources/cosmo_federated_graph/data-source.tf b/examples/data-sources/cosmo_federated_graph/data-source.tf index 998c7c6..67264a7 100644 --- a/examples/data-sources/cosmo_federated_graph/data-source.tf +++ b/examples/data-sources/cosmo_federated_graph/data-source.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_federated_graph" "test" { name = var.name namespace = var.namespace diff --git a/examples/data-sources/cosmo_federated_graph/provider.tf b/examples/data-sources/cosmo_federated_graph/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/data-sources/cosmo_federated_graph/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/data-sources/cosmo_monograph/provider.tf b/examples/data-sources/cosmo_monograph/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/data-sources/cosmo_monograph/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/data-sources/cosmo_namespace/data-source.tf b/examples/data-sources/cosmo_namespace/data-source.tf index 02caa70..b808f45 100644 --- a/examples/data-sources/cosmo_namespace/data-source.tf +++ b/examples/data-sources/cosmo_namespace/data-source.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_namespace" "test" { name = var.name } \ No newline at end of file diff --git a/examples/data-sources/cosmo_namespace/provider.tf b/examples/data-sources/cosmo_namespace/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/data-sources/cosmo_namespace/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/data-sources/cosmo_subgraph/data-source.tf b/examples/data-sources/cosmo_subgraph/data-source.tf index 569082f..3298fa5 100644 --- a/examples/data-sources/cosmo_subgraph/data-source.tf +++ b/examples/data-sources/cosmo_subgraph/data-source.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_subgraph" "test" { name = var.name namespace = var.namespace diff --git a/examples/data-sources/cosmo_subgraph/provider.tf b/examples/data-sources/cosmo_subgraph/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/data-sources/cosmo_subgraph/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/provider/_provider.tf b/examples/provider/_provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/provider/_provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/provider/main.tf b/examples/provider/main.tf index 985a634..f6055a1 100644 --- a/examples/provider/main.tf +++ b/examples/provider/main.tf @@ -27,6 +27,15 @@ module "resource_cosmo_federated_graph" { depends_on = [module.resource_cosmo_subgraph] } +module "resource_cosmo_contract" { + source = "../resources/cosmo_contract" + + name = "terraform-contract-demo" + namespace = module.resource_cosmo_namespace.name + routing_url = module.resource_cosmo_federated_graph.routing_url + source_graph_name = module.resource_cosmo_federated_graph.name +} + module "data_cosmo_federated_graph" { source = "../data-sources/cosmo_federated_graph" diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index a9f0a01..14e4db3 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - provider "cosmo" { api_url = var.api_url api_key = var.api_key diff --git a/examples/resources/cosmo_contract/outputs.tf b/examples/resources/cosmo_contract/outputs.tf new file mode 100644 index 0000000..2d627ec --- /dev/null +++ b/examples/resources/cosmo_contract/outputs.tf @@ -0,0 +1,15 @@ +output "name" { + value = cosmo_contract.test.name +} + +output "id" { + value = cosmo_contract.test.id +} + +output "namespace" { + value = cosmo_contract.test.namespace +} + +output "routing_url" { + value = cosmo_contract.test.routing_url +} \ No newline at end of file diff --git a/examples/resources/cosmo_contract/provider.tf b/examples/resources/cosmo_contract/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/resources/cosmo_contract/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/resources/cosmo_contract/resource.tf b/examples/resources/cosmo_contract/resource.tf new file mode 100644 index 0000000..8acf19a --- /dev/null +++ b/examples/resources/cosmo_contract/resource.tf @@ -0,0 +1,6 @@ +resource "cosmo_contract" "test" { + name = var.name + namespace = var.namespace + source = var.source_graph_name + routing_url = var.routing_url +} diff --git a/examples/resources/cosmo_contract/variables.tf b/examples/resources/cosmo_contract/variables.tf new file mode 100644 index 0000000..700c2c3 --- /dev/null +++ b/examples/resources/cosmo_contract/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "routing_url" { + type = string +} + +variable "namespace" { + type = string +} + +variable "source_graph_name" { + type = string +} \ No newline at end of file diff --git a/examples/resources/cosmo_federated_graph/provider.tf b/examples/resources/cosmo_federated_graph/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/resources/cosmo_federated_graph/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/resources/cosmo_federated_graph/resource.tf b/examples/resources/cosmo_federated_graph/resource.tf index 2d4139e..a887c4d 100644 --- a/examples/resources/cosmo_federated_graph/resource.tf +++ b/examples/resources/cosmo_federated_graph/resource.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_federated_graph" "test" { name = var.name routing_url = var.routing_url diff --git a/examples/resources/cosmo_namespace/provider.tf b/examples/resources/cosmo_namespace/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/resources/cosmo_namespace/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/resources/cosmo_namespace/resource.tf b/examples/resources/cosmo_namespace/resource.tf index b62ef6c..ce9df57 100644 --- a/examples/resources/cosmo_namespace/resource.tf +++ b/examples/resources/cosmo_namespace/resource.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_namespace" "test" { name = var.name } \ No newline at end of file diff --git a/examples/resources/cosmo_router_token/provider.tf b/examples/resources/cosmo_router_token/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/resources/cosmo_router_token/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/resources/cosmo_router_token/resource.tf b/examples/resources/cosmo_router_token/resource.tf index 343c49e..8254aae 100644 --- a/examples/resources/cosmo_router_token/resource.tf +++ b/examples/resources/cosmo_router_token/resource.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_router_token" "test" { name = var.name graph_name = var.graph_name diff --git a/examples/resources/cosmo_subgraph/provider.tf b/examples/resources/cosmo_subgraph/provider.tf new file mode 100644 index 0000000..675cdcc --- /dev/null +++ b/examples/resources/cosmo_subgraph/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + diff --git a/examples/resources/cosmo_subgraph/resource.tf b/examples/resources/cosmo_subgraph/resource.tf index 23514ea..3f234f2 100644 --- a/examples/resources/cosmo_subgraph/resource.tf +++ b/examples/resources/cosmo_subgraph/resource.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - resource "cosmo_subgraph" "test" { name = var.name namespace = var.namespace From 40363a36f27d388cc52b897e99817d44bc8b2d9c Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Thu, 19 Sep 2024 20:47:15 +0200 Subject: [PATCH 03/27] chore: extend example schema --- docs/resources/subgraph.md | 1 + examples/provider/main.tf | 1 + examples/provider/schema.graphql | 12 ++++++++++++ examples/resources/cosmo_subgraph/resource.tf | 1 + examples/resources/cosmo_subgraph/variables.tf | 4 ++++ 5 files changed, 19 insertions(+) create mode 100644 examples/provider/schema.graphql diff --git a/docs/resources/subgraph.md b/docs/resources/subgraph.md index c9d1407..e710939 100644 --- a/docs/resources/subgraph.md +++ b/docs/resources/subgraph.md @@ -21,6 +21,7 @@ resource "cosmo_subgraph" "test" { namespace = var.namespace routing_url = var.routing_url labels = var.labels + schema = var.schema } ``` diff --git a/examples/provider/main.tf b/examples/provider/main.tf index f6055a1..3ce510a 100644 --- a/examples/provider/main.tf +++ b/examples/provider/main.tf @@ -10,6 +10,7 @@ module "resource_cosmo_subgraph" { name = "subgraph-1" namespace = module.resource_cosmo_namespace.name routing_url = "http://example.com/routing" + schema = file("schema.graphql") labels = { "team" = "backend" "stage" = "dev" diff --git a/examples/provider/schema.graphql b/examples/provider/schema.graphql new file mode 100644 index 0000000..052e6f6 --- /dev/null +++ b/examples/provider/schema.graphql @@ -0,0 +1,12 @@ +type Query { + hello: String +} + +type Mutation { + createUser(name: String!): User +} + +type User { + id: ID! + name: String! +} diff --git a/examples/resources/cosmo_subgraph/resource.tf b/examples/resources/cosmo_subgraph/resource.tf index 3f234f2..cd87556 100644 --- a/examples/resources/cosmo_subgraph/resource.tf +++ b/examples/resources/cosmo_subgraph/resource.tf @@ -3,4 +3,5 @@ resource "cosmo_subgraph" "test" { namespace = var.namespace routing_url = var.routing_url labels = var.labels + schema = var.schema } \ No newline at end of file diff --git a/examples/resources/cosmo_subgraph/variables.tf b/examples/resources/cosmo_subgraph/variables.tf index 05696bd..2b39cef 100644 --- a/examples/resources/cosmo_subgraph/variables.tf +++ b/examples/resources/cosmo_subgraph/variables.tf @@ -12,4 +12,8 @@ variable "routing_url" { variable "labels" { type = map(string) +} + +variable "schema" { + type = string } \ No newline at end of file From fb27163bfcaa54027422a20efa043d2481444140 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Thu, 19 Sep 2024 21:00:59 +0200 Subject: [PATCH 04/27] fix: linter errors --- internal/service/namespace/resource_cosmo_namespace.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/namespace/resource_cosmo_namespace.go b/internal/service/namespace/resource_cosmo_namespace.go index 2e795f1..77563ec 100644 --- a/internal/service/namespace/resource_cosmo_namespace.go +++ b/internal/service/namespace/resource_cosmo_namespace.go @@ -140,9 +140,9 @@ func (r *NamespaceResource) Update(ctx context.Context, req resource.UpdateReque return } - err = r.client.RenameNamespace(ctx, namespace.Name, data.Name.String()) - if err != nil { - utils.AddDiagnosticError(resp, ErrUpdatingNamespace, fmt.Sprintf("Could not update namespace: %s", err)) + renameApiError := r.client.RenameNamespace(ctx, namespace.Name, data.Name.String()) + if renameApiError != nil { + utils.AddDiagnosticError(resp, ErrUpdatingNamespace, fmt.Sprintf("Could not update namespace: %s", renameApiError)) return } From f9b8ae6c7024cd4b5528310682cb1b073ffdf585 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 09:25:29 +0200 Subject: [PATCH 05/27] docs: show api_url default --- docs/index.md | 4 ++-- examples/provider/provider.tf | 2 +- internal/provider/provider.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 9f15736..3a14781 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,8 @@ Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) ```terraform provider "cosmo" { - api_url = var.api_url api_key = var.api_key + api_url = var.api_url } ``` @@ -32,4 +32,4 @@ provider "cosmo" { ### Optional - `api_key` (String) The Api Key to be used: COSMO_API_KEY -- `api_url` (String) The Api Url to be used: COSMO_API_URL +- `api_url` (String) The Api Url to be used: COSMO_API_URL Leave blank to use: https://cosmo-cp.wundergraph.com diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 14e4db3..074c442 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,4 +1,4 @@ provider "cosmo" { - api_url = var.api_url api_key = var.api_key + api_url = var.api_url } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d784e62..26c169d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -59,7 +59,7 @@ Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) `, Attributes: map[string]schema.Attribute{ "api_url": schema.StringAttribute{ - MarkdownDescription: fmt.Sprintf("The Api Url to be used: %s", utils.EnvCosmoApiUrl), + MarkdownDescription: fmt.Sprintf("The Api Url to be used: %s Leave blank to use: https://cosmo-cp.wundergraph.com", utils.EnvCosmoApiUrl), Optional: true, }, "api_key": schema.StringAttribute{ From 9f48bb72b04f7a46940b5f35b29c169db03c5b93 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 09:40:42 +0200 Subject: [PATCH 06/27] docs: improve provider usage documentation --- docs/index.md | 7 ++++--- examples/provider/provider.tf | 3 ++- internal/provider/provider.go | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3a14781..fdd875a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,8 @@ Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) ```terraform provider "cosmo" { api_key = var.api_key - api_url = var.api_url + # Set to your onpremise instance if needed + # api_url = var.api_url } ``` @@ -31,5 +32,5 @@ provider "cosmo" { ### Optional -- `api_key` (String) The Api Key to be used: COSMO_API_KEY -- `api_url` (String) The Api Url to be used: COSMO_API_URL Leave blank to use: https://cosmo-cp.wundergraph.com +- `api_key` (String) The Api Key to be used: Leave blank to use the COSMO_API_KEY environment variable +- `api_url` (String) The Api Url to be used: Leave blank to use: https://cosmo-cp.wundergraph.com or use the COSMO_API_URL environment variable diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 074c442..c97928f 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,4 +1,5 @@ provider "cosmo" { api_key = var.api_key - api_url = var.api_url + # Set to your onpremise instance if needed + # api_url = var.api_url } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 26c169d..becdde9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -59,11 +59,11 @@ Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) `, Attributes: map[string]schema.Attribute{ "api_url": schema.StringAttribute{ - MarkdownDescription: fmt.Sprintf("The Api Url to be used: %s Leave blank to use: https://cosmo-cp.wundergraph.com", utils.EnvCosmoApiUrl), + MarkdownDescription: fmt.Sprintf("The Api Url to be used: Leave blank to use: https://cosmo-cp.wundergraph.com or use the %s environment variable", utils.EnvCosmoApiUrl), Optional: true, }, "api_key": schema.StringAttribute{ - MarkdownDescription: fmt.Sprintf("The Api Key to be used: %s", utils.EnvCosmoApiKey), + MarkdownDescription: fmt.Sprintf("The Api Key to be used: Leave blank to use the %s environment variable", utils.EnvCosmoApiKey), Optional: true, }, }, From fb7965d140dcadff4655e1f5c50645fac856fc1b Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 10:54:43 +0200 Subject: [PATCH 07/27] docs: remove provider template from rendered docs --- docs/data-sources/monograph.md | 9 --------- examples/data-sources/cosmo_monograph/data-source.tf | 9 --------- 2 files changed, 18 deletions(-) diff --git a/docs/data-sources/monograph.md b/docs/data-sources/monograph.md index a217c8e..1fa3ab8 100644 --- a/docs/data-sources/monograph.md +++ b/docs/data-sources/monograph.md @@ -13,15 +13,6 @@ Cosmo Monograph Data Source ## Example Usage ```terraform -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_monograph" "example" { name = var.name } diff --git a/examples/data-sources/cosmo_monograph/data-source.tf b/examples/data-sources/cosmo_monograph/data-source.tf index b7118bc..014946c 100644 --- a/examples/data-sources/cosmo_monograph/data-source.tf +++ b/examples/data-sources/cosmo_monograph/data-source.tf @@ -1,12 +1,3 @@ -terraform { - required_providers { - cosmo = { - source = "terraform.local/wundergraph/cosmo" - version = "0.0.1" - } - } -} - data "cosmo_monograph" "example" { name = var.name } \ No newline at end of file From 8a44a4ef61d34431ce9421840f672f9aa6650cfb Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:24:24 +0200 Subject: [PATCH 08/27] docs: add markdown description to contract --- internal/service/contract/resource_cosmo_contract.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/service/contract/resource_cosmo_contract.go b/internal/service/contract/resource_cosmo_contract.go index 95de763..95ea257 100644 --- a/internal/service/contract/resource_cosmo_contract.go +++ b/internal/service/contract/resource_cosmo_contract.go @@ -38,6 +38,11 @@ func (r *contractResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *contractResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ + MarkdownDescription: ` +A contract is a Terraform resource representing a single subgraph with GraphQL Federation enabled, allowing developers to build versatile, multi-audience graphs while simplifying development and ensuring maintainability. + +For more information, refer to the Cosmo Documentation at https://cosmo-docs.wundergraph.com/concepts/schema-contracts. + `, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, From b7c3fae4ec5d5914f9cff11cf2cd31392be99d49 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:24:39 +0200 Subject: [PATCH 09/27] fix: handle subgraph composition on deletion --- .../subgraph/resource_cosmo_subgraph.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/service/subgraph/resource_cosmo_subgraph.go b/internal/service/subgraph/resource_cosmo_subgraph.go index 225ecde..71ad8d1 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph.go +++ b/internal/service/subgraph/resource_cosmo_subgraph.go @@ -295,11 +295,19 @@ func (r *SubgraphResource) Delete(ctx context.Context, req resource.DeleteReques err := r.client.DeleteSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) if err != nil { - utils.AddDiagnosticError(resp, - ErrDeletingSubgraph, - fmt.Sprintf("Could not delete subgraph '%s': %s", data.Name.ValueString(), err.Error()), - ) - return + if api.IsSubgraphCompositionFailedError(err) { + utils.AddDiagnosticWarning(resp, + ErrDeletingSubgraph, + fmt.Sprintf("Could not delete subgraph '%s': %s", data.Name.ValueString(), err.Error()), + ) + return + } else { + utils.AddDiagnosticError(resp, + ErrDeletingSubgraph, + fmt.Sprintf("Could not delete subgraph '%s': %s", data.Name.ValueString(), err.Error()), + ) + return + } } utils.LogAction(ctx, "deleted", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) From dcffa723af854f54551bfa7800859b37817abf32 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:28:19 +0200 Subject: [PATCH 10/27] chore: overhaul examples, add guides --- examples/{ => guides}/cosmo-local/debug.tf | 0 examples/{ => guides}/cosmo-local/graphs.tf | 0 examples/{ => guides}/cosmo-local/main.tf | 0 examples/{ => guides}/cosmo-local/outputs.tf | 0 examples/{ => guides}/cosmo-local/provider.tf | 0 .../cosmo-local/schema/spacex-api.graphql | 0 .../cosmo-local/values/cosmo-values.yaml | 0 .../cosmo-local/values/router-values.yaml | 0 .../{ => guides}/cosmo-local/variables.tf | 0 .../guides/cosmo-monograph-contract/main.tf | 24 ++++++++++++ .../cosmo-monograph-contract}/provider.tf | 0 .../cosmo-monograph-contract/variables.tf | 39 +++++++++++++++++++ examples/guides/cosmo-monograph/main.tf | 8 ++++ examples/guides/cosmo-monograph/provider.tf | 13 +++++++ .../cosmo-monograph}/variables.tf | 0 examples/{ => guides}/cosmo/main.tf | 0 examples/{ => guides}/cosmo/provider.tf | 0 examples/{ => guides}/cosmo/variables.tf | 0 examples/resources/comso_monograph/outputs.tf | 3 -- examples/resources/cosmo_monograph/outputs.tf | 3 ++ .../resources/cosmo_monograph/provider.tf | 8 ++++ .../resource.tf | 0 .../resources/cosmo_monograph/variables.tf | 15 +++++++ 23 files changed, 110 insertions(+), 3 deletions(-) rename examples/{ => guides}/cosmo-local/debug.tf (100%) rename examples/{ => guides}/cosmo-local/graphs.tf (100%) rename examples/{ => guides}/cosmo-local/main.tf (100%) rename examples/{ => guides}/cosmo-local/outputs.tf (100%) rename examples/{ => guides}/cosmo-local/provider.tf (100%) rename examples/{ => guides}/cosmo-local/schema/spacex-api.graphql (100%) rename examples/{ => guides}/cosmo-local/values/cosmo-values.yaml (100%) rename examples/{ => guides}/cosmo-local/values/router-values.yaml (100%) rename examples/{ => guides}/cosmo-local/variables.tf (100%) create mode 100644 examples/guides/cosmo-monograph-contract/main.tf rename examples/{resources/comso_monograph => guides/cosmo-monograph-contract}/provider.tf (100%) create mode 100644 examples/guides/cosmo-monograph-contract/variables.tf create mode 100644 examples/guides/cosmo-monograph/main.tf create mode 100644 examples/guides/cosmo-monograph/provider.tf rename examples/{resources/comso_monograph => guides/cosmo-monograph}/variables.tf (100%) rename examples/{ => guides}/cosmo/main.tf (100%) rename examples/{ => guides}/cosmo/provider.tf (100%) rename examples/{ => guides}/cosmo/variables.tf (100%) delete mode 100644 examples/resources/comso_monograph/outputs.tf create mode 100644 examples/resources/cosmo_monograph/outputs.tf create mode 100644 examples/resources/cosmo_monograph/provider.tf rename examples/resources/{comso_monograph => cosmo_monograph}/resource.tf (100%) create mode 100644 examples/resources/cosmo_monograph/variables.tf diff --git a/examples/cosmo-local/debug.tf b/examples/guides/cosmo-local/debug.tf similarity index 100% rename from examples/cosmo-local/debug.tf rename to examples/guides/cosmo-local/debug.tf diff --git a/examples/cosmo-local/graphs.tf b/examples/guides/cosmo-local/graphs.tf similarity index 100% rename from examples/cosmo-local/graphs.tf rename to examples/guides/cosmo-local/graphs.tf diff --git a/examples/cosmo-local/main.tf b/examples/guides/cosmo-local/main.tf similarity index 100% rename from examples/cosmo-local/main.tf rename to examples/guides/cosmo-local/main.tf diff --git a/examples/cosmo-local/outputs.tf b/examples/guides/cosmo-local/outputs.tf similarity index 100% rename from examples/cosmo-local/outputs.tf rename to examples/guides/cosmo-local/outputs.tf diff --git a/examples/cosmo-local/provider.tf b/examples/guides/cosmo-local/provider.tf similarity index 100% rename from examples/cosmo-local/provider.tf rename to examples/guides/cosmo-local/provider.tf diff --git a/examples/cosmo-local/schema/spacex-api.graphql b/examples/guides/cosmo-local/schema/spacex-api.graphql similarity index 100% rename from examples/cosmo-local/schema/spacex-api.graphql rename to examples/guides/cosmo-local/schema/spacex-api.graphql diff --git a/examples/cosmo-local/values/cosmo-values.yaml b/examples/guides/cosmo-local/values/cosmo-values.yaml similarity index 100% rename from examples/cosmo-local/values/cosmo-values.yaml rename to examples/guides/cosmo-local/values/cosmo-values.yaml diff --git a/examples/cosmo-local/values/router-values.yaml b/examples/guides/cosmo-local/values/router-values.yaml similarity index 100% rename from examples/cosmo-local/values/router-values.yaml rename to examples/guides/cosmo-local/values/router-values.yaml diff --git a/examples/cosmo-local/variables.tf b/examples/guides/cosmo-local/variables.tf similarity index 100% rename from examples/cosmo-local/variables.tf rename to examples/guides/cosmo-local/variables.tf diff --git a/examples/guides/cosmo-monograph-contract/main.tf b/examples/guides/cosmo-monograph-contract/main.tf new file mode 100644 index 0000000..f2eb4e9 --- /dev/null +++ b/examples/guides/cosmo-monograph-contract/main.tf @@ -0,0 +1,24 @@ +module "cosmo_namespace" { + source = "../../resources/cosmo_namespace" + + name = var.monograph_namespace +} + + +module "cosmo_monograph" { + source = "../../resources/cosmo_monograph" + + monograph_name = var.monograph_name + monograph_namespace = module.cosmo_namespace.name + monograph_graph_url = var.monograph_graph_url + monograph_routing_url = var.monograph_routing_url +} + +module "cosmo_contract" { + source = "../../resources/cosmo_contract" + + name = var.contract_name + namespace = module.cosmo_namespace.name + source_graph_name = module.cosmo_monograph.name + routing_url = var.contract_routing_url +} diff --git a/examples/resources/comso_monograph/provider.tf b/examples/guides/cosmo-monograph-contract/provider.tf similarity index 100% rename from examples/resources/comso_monograph/provider.tf rename to examples/guides/cosmo-monograph-contract/provider.tf diff --git a/examples/guides/cosmo-monograph-contract/variables.tf b/examples/guides/cosmo-monograph-contract/variables.tf new file mode 100644 index 0000000..1d75275 --- /dev/null +++ b/examples/guides/cosmo-monograph-contract/variables.tf @@ -0,0 +1,39 @@ +variable "monograph_name" { + default = "tf-guide-cosmo-monograph-contract" +} + +variable "monograph_namespace" { + default = "tf-guide-cosmo-monograph-contract" +} + +variable "monograph_graph_url" { + default = "http://example.com/graphql" +} + +variable "monograph_routing_url" { + default = "http://example.com/routing" +} + +variable "contract_name" { + type = string + default = "test" +} + +variable "contract_namespace" { + type = string + default = "default" +} + +variable "contract_routing_url" { + type = string + default = "http://example.com/routing" +} + +variable "api_url" { + type = string + default = "http://example.com/graphql" +} + +variable "api_key" { + type = string +} \ No newline at end of file diff --git a/examples/guides/cosmo-monograph/main.tf b/examples/guides/cosmo-monograph/main.tf new file mode 100644 index 0000000..ef47001 --- /dev/null +++ b/examples/guides/cosmo-monograph/main.tf @@ -0,0 +1,8 @@ +module "cosmo_monograph" { + source = "../../resources/cosmo_monograph" + + monograph_name = var.monograph_name + monograph_namespace = var.monograph_namespace + monograph_graph_url = var.monograph_graph_url + monograph_routing_url = var.monograph_routing_url +} \ No newline at end of file diff --git a/examples/guides/cosmo-monograph/provider.tf b/examples/guides/cosmo-monograph/provider.tf new file mode 100644 index 0000000..ed52ff6 --- /dev/null +++ b/examples/guides/cosmo-monograph/provider.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} + +provider "cosmo" { + api_url = var.api_url + api_key = var.api_key +} \ No newline at end of file diff --git a/examples/resources/comso_monograph/variables.tf b/examples/guides/cosmo-monograph/variables.tf similarity index 100% rename from examples/resources/comso_monograph/variables.tf rename to examples/guides/cosmo-monograph/variables.tf diff --git a/examples/cosmo/main.tf b/examples/guides/cosmo/main.tf similarity index 100% rename from examples/cosmo/main.tf rename to examples/guides/cosmo/main.tf diff --git a/examples/cosmo/provider.tf b/examples/guides/cosmo/provider.tf similarity index 100% rename from examples/cosmo/provider.tf rename to examples/guides/cosmo/provider.tf diff --git a/examples/cosmo/variables.tf b/examples/guides/cosmo/variables.tf similarity index 100% rename from examples/cosmo/variables.tf rename to examples/guides/cosmo/variables.tf diff --git a/examples/resources/comso_monograph/outputs.tf b/examples/resources/comso_monograph/outputs.tf deleted file mode 100644 index d04cd56..0000000 --- a/examples/resources/comso_monograph/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "monograph_id" { - value = cosmo_monograph.example.id -} \ No newline at end of file diff --git a/examples/resources/cosmo_monograph/outputs.tf b/examples/resources/cosmo_monograph/outputs.tf new file mode 100644 index 0000000..80f9fac --- /dev/null +++ b/examples/resources/cosmo_monograph/outputs.tf @@ -0,0 +1,3 @@ +output "name" { + value = cosmo_monograph.example.name +} \ No newline at end of file diff --git a/examples/resources/cosmo_monograph/provider.tf b/examples/resources/cosmo_monograph/provider.tf new file mode 100644 index 0000000..2093005 --- /dev/null +++ b/examples/resources/cosmo_monograph/provider.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + cosmo = { + source = "terraform.local/wundergraph/cosmo" + version = "0.0.1" + } + } +} \ No newline at end of file diff --git a/examples/resources/comso_monograph/resource.tf b/examples/resources/cosmo_monograph/resource.tf similarity index 100% rename from examples/resources/comso_monograph/resource.tf rename to examples/resources/cosmo_monograph/resource.tf diff --git a/examples/resources/cosmo_monograph/variables.tf b/examples/resources/cosmo_monograph/variables.tf new file mode 100644 index 0000000..60ed5e8 --- /dev/null +++ b/examples/resources/cosmo_monograph/variables.tf @@ -0,0 +1,15 @@ +variable "monograph_name" { + default = "test" +} + +variable "monograph_namespace" { + default = "default" +} + +variable "monograph_graph_url" { + default = "http://example.com/graphql" +} + +variable "monograph_routing_url" { + default = "http://example.com/routing" +} \ No newline at end of file From ebd315f745df91734dd0be1ca9aa2c04f816c0c0 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:28:40 +0200 Subject: [PATCH 11/27] docs: regenerate --- docs/resources/contract.md | 5 ++++- docs/resources/monograph.md | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/resources/contract.md b/docs/resources/contract.md index 7dd80b0..a52356e 100644 --- a/docs/resources/contract.md +++ b/docs/resources/contract.md @@ -3,12 +3,15 @@ page_title: "cosmo_contract Resource - cosmo" subcategory: "" description: |- - + A contract is a Terraform resource representing a single subgraph with GraphQL Federation enabled, allowing developers to build versatile, multi-audience graphs while simplifying development and ensuring maintainability. + For more information, refer to the Cosmo Documentation at https://cosmo-docs.wundergraph.com/concepts/schema-contracts. --- # cosmo_contract (Resource) +A contract is a Terraform resource representing a single subgraph with GraphQL Federation enabled, allowing developers to build versatile, multi-audience graphs while simplifying development and ensuring maintainability. +For more information, refer to the Cosmo Documentation at https://cosmo-docs.wundergraph.com/concepts/schema-contracts. ## Example Usage diff --git a/docs/resources/monograph.md b/docs/resources/monograph.md index 85468fd..28101a4 100644 --- a/docs/resources/monograph.md +++ b/docs/resources/monograph.md @@ -13,7 +13,16 @@ A monograph is a resource that represents a single subgraph with GraphQL Federat For more information on monographs, please refer to the [Cosmo Documentation](https://cosmo-docs.wundergraph.com/cli/monograph). - +## Example Usage + +```terraform +resource "cosmo_monograph" "example" { + name = var.monograph_name + namespace = var.monograph_namespace + graph_url = var.monograph_graph_url + routing_url = var.monograph_routing_url +} +``` ## Schema From 7ae11b6ecfab1965aa6812e8290c780cb2149ac5 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:29:18 +0200 Subject: [PATCH 12/27] chore: add new contract tasks to Makefile --- Makefile | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index f8d48f8..ddddbc7 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,7 @@ include examples/Makefile .PHONY: e2e-cd e2e-cosmo e2e-cosmo-monograph clean e2e-apply-cd: + rm -rf examples/provider/.terraform.lock.hcl FEATURE=examples/provider make e2e-init FEATURE=examples/provider make e2e-apply @@ -82,24 +83,37 @@ e2e-clean-cd: make e2e-clean e2e-apply-cosmo: - FEATURE=examples/cosmo make e2e-init - FEATURE=examples/cosmo make e2e-apply + rm -rf examples/guides/cosmo/.terraform.lock.hcl + FEATURE=examples/guides/cosmo make e2e-init + FEATURE=examples/guides/cosmo make e2e-apply e2e-destroy-cosmo: - FEATURE=examples/cosmo make e2e-destroy + FEATURE=examples/guides/cosmo make e2e-destroy e2e-clean-cosmo: - FEATURE=examples/cosmo make e2e-clean + FEATURE=examples/guides/cosmo make e2e-clean e2e-apply-cosmo-monograph: - FEATURE=examples/resources/comso_monograph make e2e-init - FEATURE=examples/resources/comso_monograph make e2e-apply + rm -rf examples/guides/cosmo-monograph/.terraform.lock.hcl + FEATURE=examples/guides/cosmo-monograph make e2e-init + FEATURE=examples/guides/cosmo-monograph make e2e-apply e2e-destroy-cosmo-monograph: - FEATURE=examples/resources/comso_monograph make e2e-destroy + FEATURE=examples/guides/cosmo-monograph make e2e-destroy e2e-clean-cosmo-monograph: - FEATURE=examples/resources/comso_monograph make e2e-clean + FEATURE=examples/guides/cosmo-monograph make e2e-clean + +e2e-apply-cosmo-monograph-contract: + rm -rf examples/guides/cosmo-monograph-contract/.terraform.lock.hcl + FEATURE=examples/guides/cosmo-monograph-contract make e2e-init + FEATURE=examples/guides/cosmo-monograph-contract make e2e-apply + +e2e-destroy-cosmo-monograph-contract: + FEATURE=examples/guides/cosmo-monograph-contract make e2e-destroy + +e2e-clean-cosmo-monograph-contract: + FEATURE=examples/guides/cosmo-monograph-contract make e2e-clean ## Cosmo Local # Full example installing cosmo locally with a minikube kubernetes cluster @@ -108,23 +122,25 @@ e2e-clean-cosmo-monograph: # output "hosts" generated after apply e2e-apply-cosmo-local: - rm -rf examples/cosmo-local/.terraform.lock.hcl - FEATURE=examples/cosmo-local make e2e-init - FEATURE=examples/cosmo-local make e2e-apply + rm -rf examples/guides/cosmo-local/.terraform.lock.hcl + FEATURE=examples/guides/cosmo-local make e2e-init + FEATURE=examples/guides/cosmo-local make e2e-apply e2e-destroy-cosmo-local: - FEATURE=examples/cosmo-local make e2e-destroy + FEATURE=examples/guides/cosmo-local make e2e-destroy e2e-clean-cosmo-local: - FEATURE=examples/cosmo-local make e2e-clean + FEATURE=examples/guides/cosmo-local make e2e-clean ## Convenience targets to run specific e2e tests e2e-cd: e2e-apply-cd e2e-destroy-cd e2e-cosmo: e2e-apply-cosmo e2e-destroy-cosmo e2e-cosmo-monograph: e2e-apply-cosmo-monograph e2e-destroy-cosmo-monograph +e2e-cosmo-monograph-contract: e2e-apply-cosmo-monograph-contract e2e-destroy-cosmo-monograph-contract +e2e-cosmo-local: e2e-apply-cosmo-local e2e-destroy-cosmo-local -e2e: e2e-cd e2e-cosmo e2e-cosmo-monograph +e2e: e2e-cd e2e-cosmo e2e-cosmo-monograph e2e-cosmo-monograph-contract e2e-cosmo-local -clean: e2e-clean-cd e2e-clean-cosmo e2e-clean-cosmo-monograph clean-local -destroy: e2e-destroy-cd e2e-destroy-cosmo e2e-destroy-cosmo-monograph \ No newline at end of file +clean: e2e-clean-cd e2e-clean-cosmo e2e-clean-cosmo-monograph e2e-clean-cosmo-monograph-contract e2e-clean-cosmo-local clean-local +destroy: e2e-destroy-cd e2e-destroy-cosmo e2e-destroy-cosmo-monograph e2e-destroy-cosmo-monograph-contract e2e-destroy-cosmo-local \ No newline at end of file From 617c347d3b31094be59bc5f7eb2fe468927a7015 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:44:01 +0200 Subject: [PATCH 13/27] chore: make error handling more resilient in namespace --- internal/api/errors.go | 4 ++++ .../namespace/resource_cosmo_namespace.go | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/api/errors.go b/internal/api/errors.go index fe6a093..4f1608a 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -43,6 +43,10 @@ func (e *ApiError) Error() string { return fmt.Sprintf("%s: %s (status: %s)", e.Err.Error(), e.Reason, e.Status.String()) } +func NewApiErrorWithErr(statusCode common.EnumStatusCode, reason string, err error) *ApiError { + return &ApiError{Err: err, Reason: reason, Status: statusCode} +} + func handleErrorCodes(statusCode common.EnumStatusCode, reason string) *ApiError { if strings.Contains(reason, ContractCompositionFailedReason) { return &ApiError{Err: ErrContractCompositionFailed, Reason: reason, Status: statusCode} diff --git a/internal/service/namespace/resource_cosmo_namespace.go b/internal/service/namespace/resource_cosmo_namespace.go index 77563ec..f62683a 100644 --- a/internal/service/namespace/resource_cosmo_namespace.go +++ b/internal/service/namespace/resource_cosmo_namespace.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" + common "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/api" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/utils" @@ -67,7 +68,6 @@ For more information on namespaces, please refer to the [Cosmo Documentation](ht func (r *NamespaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data NamespaceResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } @@ -105,9 +105,13 @@ func (r *NamespaceResource) Read(ctx context.Context, req resource.ReadRequest, return } - namespace, err := getNamespaceByName(ctx, *r.client, data.Name.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, ErrReadingNamespace, err.Error()) + namespace, apiError := getNamespaceByName(ctx, *r.client, data.Name.ValueString()) + if apiError != nil { + if api.IsNotFoundError(apiError) { + resp.State.RemoveResource(ctx) + return + } + utils.AddDiagnosticError(resp, ErrReadingNamespace, apiError.Error()) return } @@ -168,10 +172,10 @@ func (r *NamespaceResource) Delete(ctx context.Context, req resource.DeleteReque utils.LogAction(ctx, "deleted", data.Id.ValueString(), data.Name.ValueString(), "") } -func getNamespaceByName(ctx context.Context, client api.PlatformClient, name string) (*platformv1.Namespace, error) { +func getNamespaceByName(ctx context.Context, client api.PlatformClient, name string) (*platformv1.Namespace, *api.ApiError) { namespaces, err := client.ListNamespaces(ctx) if err != nil { - return nil, fmt.Errorf("could not list namespaces: %w", err) + return nil, err } for _, namespace := range namespaces { @@ -180,9 +184,8 @@ func getNamespaceByName(ctx context.Context, client api.PlatformClient, name str } } - return nil, fmt.Errorf("namespace with name '%s' not found", name) + return nil, api.NewApiErrorWithErr(common.EnumStatusCode_ERR_NOT_FOUND, fmt.Sprintf("namespace with name '%s' not found", name), fmt.Errorf("namespace with name '%s' not found", name)) } - func (r *NamespaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } From f48683053eefdc80bded18156f0a7bdc80ba7f15 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 11:44:56 +0200 Subject: [PATCH 14/27] chore: update module paths --- examples/guides/cosmo-local/graphs.tf | 2 +- examples/guides/cosmo-local/main.tf | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/guides/cosmo-local/graphs.tf b/examples/guides/cosmo-local/graphs.tf index 972a567..47c43a0 100644 --- a/examples/guides/cosmo-local/graphs.tf +++ b/examples/guides/cosmo-local/graphs.tf @@ -16,7 +16,7 @@ locals { // this module wraps generating a federated graph and related subgraphs // the resources are deployed within the given namespace module "cosmo_federated_graph" { - source = "../../modules/cosmo-federated-graph" + source = "../../../modules/cosmo-federated-graph" namespace = "${var.stage}-${local.prefix}" router_token_name = "${var.stage}-${local.prefix}-router-token" diff --git a/examples/guides/cosmo-local/main.tf b/examples/guides/cosmo-local/main.tf index b0278bf..bd1ae1f 100644 --- a/examples/guides/cosmo-local/main.tf +++ b/examples/guides/cosmo-local/main.tf @@ -21,7 +21,7 @@ locals { // 1. Install minikube on which cosmo will be deployed module "minikube" { - source = "../../modules/minikube" + source = "../../../modules/minikube" minikube_clusters = var.minikube kubernetes_version = var.kubernetes_version } @@ -35,7 +35,7 @@ resource "time_sleep" "wait_for_minikube" { // 3. Render the cosmo charts (used for local development and debugging) module "cosmo_charts" { - source = "../../modules/charts/template" + source = "../../../modules/charts/template" chart = var.cosmo.chart depends_on = [time_sleep.wait_for_minikube] @@ -53,7 +53,7 @@ resource "kubernetes_namespace" "cosmo_namespace" { // 5. Install the cosmo helm release // see var.cosmo.release_name and var.cosmo.chart for more details module "cosmo_release" { - source = "../../modules/charts/release" + source = "../../../modules/charts/release" chart = var.cosmo.chart release_name = var.cosmo.release_name @@ -65,7 +65,7 @@ module "cosmo_release" { // see local.cosmo_router.release_name and local.cosmo_router.chart for more details // this happens after graphs.tf was applied after the router token was created module "cosmo_router_release" { - source = "../../modules/charts/release" + source = "../../../modules/charts/release" chart = local.cosmo_router.chart release_name = local.cosmo_router.release_name From 412f28623d5a7cf683bdcf68de87f47abf08bbff Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 12:26:50 +0200 Subject: [PATCH 15/27] chore: do not imply cosmo variables setup on cosmo-local --- examples/guides/cosmo-local/main.tf | 2 +- examples/guides/cosmo-local/variables.tf | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/guides/cosmo-local/main.tf b/examples/guides/cosmo-local/main.tf index bd1ae1f..935ff4c 100644 --- a/examples/guides/cosmo-local/main.tf +++ b/examples/guides/cosmo-local/main.tf @@ -28,7 +28,7 @@ module "minikube" { // 2. Wait for minikube to be ready to avoid race conditions with helm resource "time_sleep" "wait_for_minikube" { - create_duration = "10s" + create_duration = "30s" depends_on = [module.minikube] } diff --git a/examples/guides/cosmo-local/variables.tf b/examples/guides/cosmo-local/variables.tf index 9a9f128..fe5619a 100644 --- a/examples/guides/cosmo-local/variables.tf +++ b/examples/guides/cosmo-local/variables.tf @@ -94,10 +94,12 @@ variable "cosmo_router" { } variable "api_url" { - type = string + type = string + description = "The helm Charts control plane url" } variable "api_key" { - type = string + type = string + description = "The helm Charts control plane api key" } From b93344045e28534e6b40031d80c2310fd83cc1e5 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 12:27:04 +0200 Subject: [PATCH 16/27] chore: add delete tests for resources --- .../resource_cosmo_federated_graph_test.go | 4 + .../resource_cosmo_monograph_test.go | 4 + .../resource_cosmo_namespace_test.go | 4 + .../router-token/resource_cosmo_token_test.go | 4 + .../subgraph/resource_cosmo_subgraph_test.go | 75 ++++++++++++++++++- 5 files changed, 89 insertions(+), 2 deletions(-) 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 32750c3..295cd8e 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph_test.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph_test.go @@ -45,6 +45,10 @@ func TestAccFederatedGraphResource(t *testing.T) { resource.TestCheckResourceAttr("cosmo_federated_graph.test", "routing_url", updatedRoutingURL), ), }, + { + ResourceName: "cosmo_federated_graph.test", + RefreshState: true, + }, }, }) } diff --git a/internal/service/monograph/resource_cosmo_monograph_test.go b/internal/service/monograph/resource_cosmo_monograph_test.go index 8702332..be63829 100644 --- a/internal/service/monograph/resource_cosmo_monograph_test.go +++ b/internal/service/monograph/resource_cosmo_monograph_test.go @@ -41,6 +41,10 @@ func TestAccMonographResource(t *testing.T) { ResourceName: "cosmo_monograph.test", RefreshState: true, }, + { + Config: testAccMonographResourceConfig(namespace, name, graphUrl, rRoutingURL), + Destroy: true, + }, }, }) } diff --git a/internal/service/namespace/resource_cosmo_namespace_test.go b/internal/service/namespace/resource_cosmo_namespace_test.go index 953fd78..8ff83c3 100644 --- a/internal/service/namespace/resource_cosmo_namespace_test.go +++ b/internal/service/namespace/resource_cosmo_namespace_test.go @@ -32,6 +32,10 @@ func TestAccNamespaceResource(t *testing.T) { Config: testAccNamespaceResourceConfig(updatedName), ExpectError: regexp.MustCompile(`Changing the namespace name requires recreation.`), }, + { + Config: testAccNamespaceResourceConfig(rName), + Destroy: true, + }, }, }) } diff --git a/internal/service/router-token/resource_cosmo_token_test.go b/internal/service/router-token/resource_cosmo_token_test.go index f8da0cc..c6cdbb0 100644 --- a/internal/service/router-token/resource_cosmo_token_test.go +++ b/internal/service/router-token/resource_cosmo_token_test.go @@ -35,6 +35,10 @@ func TestAccTokenResource(t *testing.T) { ResourceName: "cosmo_router_token.test", RefreshState: true, }, + { + Config: testAccTokenResourceConfig(namespace, name), + Destroy: true, + }, }, }) } diff --git a/internal/service/subgraph/resource_cosmo_subgraph_test.go b/internal/service/subgraph/resource_cosmo_subgraph_test.go index e2c8871..24ae641 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph_test.go +++ b/internal/service/subgraph/resource_cosmo_subgraph_test.go @@ -10,11 +10,13 @@ import ( ) func TestAccSubgraphResource(t *testing.T) { - federatedGraphName := acctest.RandomWithPrefix("test-subgraph") namespace := acctest.RandomWithPrefix("test-namespace") - subgraphName := acctest.RandomWithPrefix("test-subgraph") + + federatedGraphName := acctest.RandomWithPrefix("test-subgraph") federatedGraphRoutingURL := "https://example.com" + subgraphName := acctest.RandomWithPrefix("test-subgraph") + routingURL := "https://example.com" updatedRoutingURL := "https://updated-example.com" @@ -42,6 +44,57 @@ func TestAccSubgraphResource(t *testing.T) { ResourceName: "cosmo_subgraph.test", RefreshState: true, }, + { + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL), + Destroy: true, + }, + }, + }) +} + +// these subgraphs should be deleteable without any issues +func TestAccStandaloneSubgraphResource(t *testing.T) { + namespace := acctest.RandomWithPrefix("test-namespace") + + federatedGraphName := acctest.RandomWithPrefix("test-subgraph") + federatedGraphRoutingURL := "https://example.com" + + subgraphName := acctest.RandomWithPrefix("test-subgraph") + + routingURL := "https://example.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL), + 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"), + ), + }, + { + Config: testStandaloneSubgraph(namespace, subgraphName, routingURL), + 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"), + ), + }, + { + ResourceName: "cosmo_subgraph.test", + RefreshState: true, + }, + { + Config: testStandaloneSubgraph(namespace, subgraphName, routingURL), + Destroy: true, + }, }, }) } @@ -72,3 +125,21 @@ resource "cosmo_subgraph" "test" { } `, namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL) } + +func testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL string) string { + return fmt.Sprintf(` +resource "cosmo_namespace" "test" { + name = "%s" +} + +resource "cosmo_subgraph" "test" { + name = "%s" + namespace = cosmo_namespace.test.name + routing_url = "%s" + labels = { + "team" = "backend", + "stage" = "dev" + } +} +`, namespace, subgraphName, subgraphRoutingURL) +} From 1fcb938a3606542b4751ab60db7be3d4ba91b86b Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 12:27:21 +0200 Subject: [PATCH 17/27] fix: do not run cosmo-local with e2e tests --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ddddbc7..4c9b180 100644 --- a/Makefile +++ b/Makefile @@ -140,7 +140,7 @@ e2e-cosmo-monograph: e2e-apply-cosmo-monograph e2e-destroy-cosmo-monograph e2e-cosmo-monograph-contract: e2e-apply-cosmo-monograph-contract e2e-destroy-cosmo-monograph-contract e2e-cosmo-local: e2e-apply-cosmo-local e2e-destroy-cosmo-local -e2e: e2e-cd e2e-cosmo e2e-cosmo-monograph e2e-cosmo-monograph-contract e2e-cosmo-local +e2e: e2e-cd e2e-cosmo e2e-cosmo-monograph e2e-cosmo-monograph-contract -clean: e2e-clean-cd e2e-clean-cosmo e2e-clean-cosmo-monograph e2e-clean-cosmo-monograph-contract e2e-clean-cosmo-local clean-local +clean: e2e-clean-cd e2e-clean-cosmo e2e-clean-cosmo-monograph e2e-clean-cosmo-monograph-contract clean-local destroy: e2e-destroy-cd e2e-destroy-cosmo e2e-destroy-cosmo-monograph e2e-destroy-cosmo-monograph-contract e2e-destroy-cosmo-local \ No newline at end of file From fc7f04b16bb5e176b8dffec4237aad21e0832ce4 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 13:23:00 +0200 Subject: [PATCH 18/27] chore: adjust error handling also in federated graph --- internal/acceptance/testing.go | 683 ++++++++++++++++++ internal/api/errors.go | 7 + internal/api/federated_graph.go | 2 +- internal/service/federated-graph/errors.go | 1 + .../resource_cosmo_federated_graph.go | 56 +- .../subgraph/resource_cosmo_subgraph.go | 58 +- .../subgraph/resource_cosmo_subgraph_test.go | 81 ++- 7 files changed, 830 insertions(+), 58 deletions(-) diff --git a/internal/acceptance/testing.go b/internal/acceptance/testing.go index 586459f..d6c9077 100644 --- a/internal/acceptance/testing.go +++ b/internal/acceptance/testing.go @@ -21,3 +21,686 @@ func TestAccPreCheck(t *testing.T) { // about the appropriate environment variables being set are common to see in a pre-check // function. } + +const ( + TestAccValidSubgraphSchema = ` +directive @rateLimit(max: Int, window: String, message: String, identityArgs: [String]) on FIELD_DEFINITION + +type Address { + address: String + city: String + state: String +} + +type Capsule { + id: ID + landings: Int + missions: [CapsuleMission] + original_launch: Date + reuse_count: Int + status: String + type: String + dragon: Dragon +} + +type CapsuleMission { + flight: Int + name: String +} + +input CapsulesFind { + id: ID + landings: Int + mission: String + original_launch: Date + reuse_count: Int + status: String + type: String +} + +type Core { + asds_attempts: Int + asds_landings: Int + block: Int + id: ID + missions: [CapsuleMission] + original_launch: Date + reuse_count: Int + rtls_attempts: Int + rtls_landings: Int + status: String + water_landing: Boolean +} + +type CoreMission { + name: String + flight: Int +} + +input CoresFind { + asds_attempts: Int + asds_landings: Int + block: Int + id: String + missions: String + original_launch: Date + reuse_count: Int + rtls_attempts: Int + rtls_landings: Int + status: String + water_landing: Boolean +} + +scalar Date + +type Distance { + feet: Float + meters: Float +} + +type Dragon { + active: Boolean + crew_capacity: Int + description: String + diameter: Distance + dry_mass_kg: Int + dry_mass_lb: Int + first_flight: String + heat_shield: DragonHeatShield + height_w_trunk: Distance + id: ID + launch_payload_mass: Mass + launch_payload_vol: Volume + name: String + orbit_duration_yr: Int + pressurized_capsule: DragonPressurizedCapsule + return_payload_mass: Mass + return_payload_vol: Volume + sidewall_angle_deg: Float + thrusters: [DragonThrust] + trunk: DragonTrunk + type: String + wikipedia: String +} + +type DragonHeatShield { + dev_partner: String + material: String + size_meters: Float + temp_degrees: Int +} + +type DragonPressurizedCapsule { + payload_volume: Volume +} + +type DragonThrust { + amount: Int + fuel_1: String + fuel_2: String + pods: Int + thrust: Force + type: String +} + +type DragonTrunk { + cargo: DragonTrunkCargo + trunk_volume: Volume +} + +type DragonTrunkCargo { + solar_array: Int + unpressurized_cargo: Boolean +} + +type Force { + kN: Float + lbf: Float +} + +type HistoriesResult { + result: Result + data: [History] +} + +type History { + details: String + event_date_unix: Date + event_date_utc: Date + id: ID + links: Link + title: String + flight: Launch +} + +input HistoryFind { + end: Date + flight_number: Int + id: ID + start: Date +} + +type Info { + ceo: String + coo: String + cto_propulsion: String + cto: String + employees: Int + founded: Int + founder: String + headquarters: Address + launch_sites: Int + links: InfoLinks + name: String + summary: String + test_sites: Int + valuation: Float + vehicles: Int +} + +type InfoLinks { + elon_twitter: String + flickr: String + twitter: String + website: String +} + +type Landpad { + attempted_landings: String + details: String + full_name: String + id: ID + landing_type: String + location: Location + status: String + successful_landings: String + wikipedia: String +} + +type Launch { + details: String + id: ID + is_tentative: Boolean + launch_date_local: Date + launch_date_unix: Date + launch_date_utc: Date + launch_site: LaunchSite + launch_success: Boolean + launch_year: String + links: LaunchLinks + mission_id: [String] + mission_name: String + rocket: LaunchRocket + static_fire_date_unix: Date + static_fire_date_utc: Date + telemetry: LaunchTelemetry + tentative_max_precision: String + upcoming: Boolean + ships: [Ship] +} + +type LaunchesPastResult { + result: Result + data: [Launch] +} + +input LaunchFind { + apoapsis_km: Float + block: Int + cap_serial: String + capsule_reuse: String + core_flight: Int + core_reuse: String + core_serial: String + customer: String + eccentricity: Float + end: Date + epoch: Date + fairings_recovered: String + fairings_recovery_attempt: String + fairings_reuse: String + fairings_reused: String + fairings_ship: String + gridfins: String + id: ID + inclination_deg: Float + land_success: String + landing_intent: String + landing_type: String + landing_vehicle: String + launch_date_local: Date + launch_date_utc: Date + launch_success: String + launch_year: String + legs: String + lifespan_years: Float + longitude: Float + manufacturer: String + mean_motion: Float + mission_id: String + mission_name: String + nationality: String + norad_id: Int + orbit: String + payload_id: String + payload_type: String + periapsis_km: Float + period_min: Float + raan: Float + reference_system: String + regime: String + reused: String + rocket_id: String + rocket_name: String + rocket_type: String + second_stage_block: String + semi_major_axis_km: Float + ship: String + side_core1_reuse: String + side_core2_reuse: String + site_id: String + site_name_long: String + site_name: String + start: Date + tbd: String + tentative_max_precision: String + tentative: String +} + +type LaunchLinks { + article_link: String + flickr_images: [String] + mission_patch_small: String + mission_patch: String + presskit: String + reddit_campaign: String + reddit_launch: String + reddit_media: String + reddit_recovery: String + video_link: String + wikipedia: String +} + +type Launchpad { + attempted_launches: Int + details: String + id: ID + location: Location + name: String + status: String + successful_launches: Int + vehicles_launched: [Rocket] + wikipedia: String +} + +type LaunchRocket { + fairings: LaunchRocketFairings + first_stage: LaunchRocketFirstStage + rocket_name: String + rocket_type: String + rocket: Rocket + second_stage: LaunchRocketSecondStage +} + +type LaunchRocketFairings { + recovered: Boolean + recovery_attempt: Boolean + reused: Boolean + ship: String +} + +type LaunchRocketFirstStage { + cores: [LaunchRocketFirstStageCore] +} + +type LaunchRocketFirstStageCore { + block: Int + core: Core + flight: Int + gridfins: Boolean + land_success: Boolean + landing_intent: Boolean + landing_type: String + landing_vehicle: String + legs: Boolean + reused: Boolean +} + +type LaunchRocketSecondStage { + block: Int + payloads: [Payload] +} + +type LaunchSite { + site_id: String + site_name_long: String + site_name: String +} + +type LaunchTelemetry { + flight_club: String +} + +type Link { + article: String + reddit: String + wikipedia: String +} + +type Location { + latitude: Float + longitude: Float + name: String + region: String +} + +type Mass { + kg: Int + lb: Int +} + +type Mission { + description: String + id: ID + manufacturers: [String] + name: String + twitter: String + website: String + wikipedia: String + payloads: [Payload] +} + +type MissionResult { + result: Result + data: [Mission] +} + +input MissionsFind { + id: ID + manufacturer: String + name: String + payload_id: String +} + +scalar ObjectID + +type Payload { + customers: [String] + id: ID + manufacturer: String + nationality: String + norad_id: [Int] + orbit_params: PayloadOrbitParams + orbit: String + payload_mass_kg: Float + payload_mass_lbs: Float + payload_type: String + reused: Boolean +} + +type PayloadOrbitParams { + apoapsis_km: Float + arg_of_pericenter: Float + eccentricity: Float + epoch: Date + inclination_deg: Float + lifespan_years: Float + longitude: Float + mean_anomaly: Float + mean_motion: Float + periapsis_km: Float + period_min: Float + raan: Float + reference_system: String + regime: String + semi_major_axis_km: Float +} + +input PayloadsFind { + apoapsis_km: Float + customer: String + eccentricity: Float + epoch: Date + inclination_deg: Float + lifespan_years: Float + longitude: Float + manufacturer: String + mean_motion: Float + nationality: String + norad_id: Int + orbit: String + payload_id: ID + payload_type: String + periapsis_km: Float + period_min: Float + raan: Float + reference_system: String + regime: String + reused: Boolean + semi_major_axis_km: Float +} + +type Query { + capsules(find: CapsulesFind, limit: Int, offset: Int, order: String, sort: String): [Capsule] + capsulesPast(find: CapsulesFind, limit: Int, offset: Int, order: String, sort: String): [Capsule] + capsulesUpcoming(find: CapsulesFind, limit: Int, offset: Int, order: String, sort: String): [Capsule] + capsule(id: ID!): Capsule + company: Info + cores(find: CoresFind, limit: Int, offset: Int, order: String, sort: String): [Core] + coresPast(find: CoresFind, limit: Int, offset: Int, order: String, sort: String): [Core] + coresUpcoming(find: CoresFind, limit: Int, offset: Int, order: String, sort: String): [Core] + core(id: ID!): Core + dragons(limit: Int, offset: Int): [Dragon] + dragon(id: ID!): Dragon + histories(find: HistoryFind, limit: Int, offset: Int, order: String, sort: String): [History] + historiesResult(find: HistoryFind, limit: Int, offset: Int, order: String, sort: String): HistoriesResult + history(id: ID!): History + landpads(limit: Int, offset: Int): [Landpad] + landpad(id: ID!): Landpad + launches(find: LaunchFind, limit: Int, offset: Int, order: String, sort: String): [Launch] + launchesPast(find: LaunchFind, limit: Int, offset: Int, order: String, sort: String): [Launch] + launchesPastResult(find: LaunchFind, limit: Int, offset: Int, order: String, sort: String): LaunchesPastResult + launchesUpcoming(find: LaunchFind, limit: Int, offset: Int, order: String, sort: String): [Launch] + launch(id: ID!): Launch + launchLatest(offset: Int): Launch + launchNext(offset: Int): Launch + launchpads(limit: Int, offset: Int): [Launchpad] + launchpad(id: ID!): Launchpad + missions(find: MissionsFind, limit: Int, offset: Int): [Mission] + missionsResult(find: MissionsFind, limit: Int, offset: Int): MissionResult + mission(id: ID!): Mission + payloads(find: PayloadsFind, limit: Int, offset: Int, order: String, sort: String): [Payload] + payload(id: ID!): Payload + roadster: Roadster + rockets(limit: Int, offset: Int): [Rocket] + rocketsResult(limit: Int, offset: Int): RocketsResult + rocket(id: ID!): Rocket + ships(find: ShipsFind, limit: Int, offset: Int, order: String, sort: String): [Ship] + shipsResult(find: ShipsFind, limit: Int, offset: Int, order: String, sort: String): ShipsResult + ship(id: ID!): Ship +} + +type Result { + totalCount: Int +} + +type Roadster { + apoapsis_au: Float + details: String + earth_distance_km: Float + earth_distance_mi: Float + eccentricity: Float + epoch_jd: Float + inclination: Float + launch_date_unix: Date + launch_date_utc: Date + launch_mass_kg: Int + launch_mass_lbs: Int + longitude: Float + mars_distance_km: Float + mars_distance_mi: Float + name: String + norad_id: Int + orbit_type: Float + periapsis_arg: Float + periapsis_au: Float + period_days: Float + semi_major_axis_au: Float + speed_kph: Float + speed_mph: Float + wikipedia: String +} + +type Rocket { + active: Boolean + boosters: Int + company: String + cost_per_launch: Int + country: String + description: String + diameter: Distance + engines: RocketEngines + first_flight: Date + first_stage: RocketFirstStage + height: Distance + id: ID + landing_legs: RocketLandingLegs + mass: Mass + name: String + payload_weights: [RocketPayloadWeight] + second_stage: RocketSecondStage + stages: Int + success_rate_pct: Int + type: String + wikipedia: String +} + +type RocketEngines { + number: Int + type: String + version: String + layout: String + engine_loss_max: String + propellant_1: String + propellant_2: String + thrust_sea_level: Force + thrust_vacuum: Force + thrust_to_weight: Float +} + +type RocketFirstStage { + burn_time_sec: Int + engines: Int + fuel_amount_tons: Float + reusable: Boolean + thrust_sea_level: Force + thrust_vacuum: Force +} + +type RocketLandingLegs { + number: Int + material: String +} + +type RocketPayloadWeight { + id: String + kg: Int + lb: Int + name: String +} + +type RocketSecondStage { + burn_time_sec: Int + engines: Int + fuel_amount_tons: Float + payloads: RocketSecondStagePayloads + thrust: Force +} + +type RocketSecondStagePayloadCompositeFairing { + height: Distance + diameter: Distance +} + +type RocketSecondStagePayloads { + option_1: String + composite_fairing: RocketSecondStagePayloadCompositeFairing +} + +type RocketsResult { + result: Result + data: [Rocket] +} + +type Ship { + abs: Int + active: Boolean + attempted_landings: Int + class: Int + course_deg: Int + home_port: String + id: ID + image: String + imo: Int + missions: [ShipMission] + mmsi: Int + model: String + name: String + position: ShipLocation + roles: [String] + speed_kn: Float + status: String + successful_landings: Int + type: String + url: String + weight_kg: Int + weight_lbs: Int + year_built: Int +} + +type ShipLocation { + latitude: Float + longitude: Float +} + +type ShipMission { + flight: String + name: String +} + +input ShipsFind { + id: ID + name: String + model: String + type: String + role: String + active: Boolean + imo: Int + mmsi: Int + abs: Int + class: Int + weight_lbs: Int + weight_kg: Int + year_built: Int + home_port: String + status: String + speed_kn: Int + course_deg: Int + latitude: Float + longitude: Float + successful_landings: Int + attempted_landings: Int + mission: String +} + +type ShipsResult { + result: Result + data: [Ship] +} + +type Volume { + cubic_feet: Int + cubic_meters: Int +} +` +) diff --git a/internal/api/errors.go b/internal/api/errors.go index 4f1608a..00eaa4f 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -15,6 +15,7 @@ var ( ErrSubgraphCompositionFailed = errors.New("ErrSubgraphCompositionFailed") ErrEmptyMsg = fmt.Errorf("ErrEmptyMsg") ErrContractCompositionFailed = fmt.Errorf("ErrContractCompositionFailed") + ErrInvalidSubgraphSchema = fmt.Errorf("ErrInvalidSubgraphSchema") ) const ( @@ -29,6 +30,10 @@ func IsSubgraphCompositionFailedError(err *ApiError) bool { return errors.Is(err.Err, ErrSubgraphCompositionFailed) } +func IsInvalidSubgraphSchemaError(err *ApiError) bool { + return errors.Is(err.Err, ErrInvalidSubgraphSchema) +} + func IsContractCompositionFailedError(err *ApiError) bool { return errors.Is(err.Err, ErrContractCompositionFailed) } @@ -59,6 +64,8 @@ func handleErrorCodes(statusCode common.EnumStatusCode, reason string) *ApiError return &ApiError{Err: ErrSubgraphCompositionFailed, Reason: reason, Status: statusCode} case common.EnumStatusCode_ERR_NOT_FOUND: return &ApiError{Err: ErrNotFound, Reason: reason, Status: statusCode} + case common.EnumStatusCode_ERR_INVALID_SUBGRAPH_SCHEMA: + return &ApiError{Err: ErrInvalidSubgraphSchema, Reason: reason, Status: statusCode} case common.EnumStatusCode_ERR: return &ApiError{Err: ErrGeneral, Reason: reason, Status: statusCode} default: diff --git a/internal/api/federated_graph.go b/internal/api/federated_graph.go index fb054a2..519d9f4 100644 --- a/internal/api/federated_graph.go +++ b/internal/api/federated_graph.go @@ -43,7 +43,7 @@ func (p *PlatformClient) CreateFederatedGraph(ctx context.Context, admissionWebh return response.Msg, nil } -func (p *PlatformClient) UpdateFederatedGraph(ctx context.Context, admissionWebhookSecret *string, graph *platformv1.FederatedGraph) (*platformv1.UpdateFederatedGraphResponse, error) { +func (p *PlatformClient) UpdateFederatedGraph(ctx context.Context, admissionWebhookSecret *string, graph *platformv1.FederatedGraph) (*platformv1.UpdateFederatedGraphResponse, *ApiError) { var admissionWebhookURL *string if graph.AdmissionWebhookUrl != nil { admissionWebhookURL = graph.AdmissionWebhookUrl diff --git a/internal/service/federated-graph/errors.go b/internal/service/federated-graph/errors.go index 9507bde..998040d 100644 --- a/internal/service/federated-graph/errors.go +++ b/internal/service/federated-graph/errors.go @@ -10,6 +10,7 @@ const ( ErrUpdatingGraph = "Error Updating Federated Graph" ErrDeletingGraph = "Error Deleting Federated Graph" ErrUnexpectedDataSourceType = "Unexpected Data Source Configure Type" + ErrCompositionErrors = "Composition Errors" ) const ( diff --git a/internal/service/federated-graph/resource_cosmo_federated_graph.go b/internal/service/federated-graph/resource_cosmo_federated_graph.go index 38f47c7..524386c 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph.go @@ -14,8 +14,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" + common "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/common" platformv1 "github.com/wundergraph/cosmo/connect-go/gen/proto/wg/cosmo/platform/v1" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/api" "github.com/wundergraph/cosmo/terraform-provider-cosmo/internal/utils" @@ -135,16 +135,15 @@ func (r *FederatedGraphResource) Create(ctx context.Context, req resource.Create return } - response, err := r.createFederatedGraph(ctx, data, resp) - if err != nil { - utils.AddDiagnosticError(resp, ErrCreatingGraph, err.Error()) - return - } - - compositionErrors := response.Graph.GetCompositionErrors() - if len(compositionErrors) > 0 { - utils.AddDiagnosticWarning(resp, ErrCompositionError, fmt.Sprintf("Composition errors: %v", compositionErrors)) - return + response, apiError := r.createFederatedGraph(ctx, data, resp) + if apiError != nil { + if api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticWarning(resp, ErrCompositionError, apiError.Error()) + } else { + utils.AddDiagnosticError(resp, ErrCreatingGraph, apiError.Error()) + return + } + utils.AddDiagnosticWarning(resp, ErrCompositionError, apiError.Error()) } graph := response.Graph @@ -182,11 +181,6 @@ func (r *FederatedGraphResource) Read(ctx context.Context, req resource.ReadRequ return } - if apiResponse.GetResponse().Code != common.EnumStatusCode_OK { - utils.AddDiagnosticError(resp, ErrReadingGraph, fmt.Sprintf("Failed to retrieve federated graph with status code: %v, details: %s", apiResponse.GetResponse().Code, apiResponse.GetResponse().GetDetails())) - return - } - graph := apiResponse.Graph data.Id = types.StringValue(graph.GetId()) data.Name = types.StringValue(graph.GetName()) @@ -236,10 +230,20 @@ func (r *FederatedGraphResource) Update(ctx context.Context, req resource.Update admissionWebhookSecret = data.AdmissionWebhookSecret.ValueStringPointer() } - apiResponse, err := r.client.UpdateFederatedGraph(ctx, admissionWebhookSecret, &graph) - if err != nil { - utils.AddDiagnosticError(resp, ErrUpdatingGraph, fmt.Sprintf("error: %s, graph name: %s, graph namespace: %s", err, graph.Name, graph.Namespace)) - return + apiResponse, apiError := r.client.UpdateFederatedGraph(ctx, admissionWebhookSecret, &graph) + if apiError != nil { + if api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrCompositionErrors, + apiError.Error(), + ) + } else { + utils.AddDiagnosticError(resp, + ErrUpdatingGraph, + apiError.Error(), + ) + return + } } if len(apiResponse.CompositionErrors) > 0 { @@ -279,10 +283,10 @@ func (r *FederatedGraphResource) ImportState(ctx context.Context, req resource.I resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -func (r *FederatedGraphResource) createFederatedGraph(ctx context.Context, data FederatedGraphResourceModel, resp *resource.CreateResponse) (*platformv1.GetFederatedGraphByNameResponse, error) { +func (r *FederatedGraphResource) createFederatedGraph(ctx context.Context, data FederatedGraphResourceModel, resp *resource.CreateResponse) (*platformv1.GetFederatedGraphByNameResponse, *api.ApiError) { labelMatchers, err := utils.ConvertAndValidateLabelMatchers(data.LabelMatchers, resp) if err != nil { - return nil, err + return nil, &api.ApiError{Err: err, Reason: "CreateFederatedGraph", Status: common.EnumStatusCode_ERR} } apiGraph := platformv1.FederatedGraph{ @@ -307,12 +311,16 @@ func (r *FederatedGraphResource) createFederatedGraph(ctx context.Context, data _, apiError := r.client.CreateFederatedGraph(ctx, admissionWebhookSecret, &apiGraph) if apiError != nil { - return nil, fmt.Errorf("could not create federated graph: %w", apiError) + if api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticWarning(resp, ErrCompositionError, apiError.Error()) + } else { + return nil, apiError + } } response, apiError := r.client.GetFederatedGraph(ctx, apiGraph.Name, apiGraph.Namespace) if apiError != nil { - return nil, fmt.Errorf("could not retrieve federated graph: %w", apiError) + return nil, apiError } utils.DebugAction(ctx, DebugCreate, data.Name.ValueString(), data.Namespace.ValueString(), map[string]interface{}{ diff --git a/internal/service/subgraph/resource_cosmo_subgraph.go b/internal/service/subgraph/resource_cosmo_subgraph.go index 71ad8d1..b59f3a4 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph.go +++ b/internal/service/subgraph/resource_cosmo_subgraph.go @@ -226,23 +226,6 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques } } - if data.Schema.ValueString() != "" { - apiResponse, err := r.client.PublishSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) - if err != nil { - utils.AddDiagnosticError(resp, - ErrPublishingSubgraph, - fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), err.Error()), - ) - return - } - if apiResponse.HasChanged != nil && *apiResponse.HasChanged { - resp.Diagnostics.AddWarning( - ErrSubgraphSchemaChanged, - fmt.Sprintf("The schema for subgraph '%s' has changed and was published.", data.Name.ValueString()), - ) - } - } - var unsetLabels *bool if data.UnsetLabels.ValueBool() { unsetLabels = &[]bool{true}[0] @@ -257,13 +240,12 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques ErrUpdatingSubgraph, fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), ) - } else { - utils.AddDiagnosticError(resp, - ErrUpdatingSubgraph, - fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), - ) - return } + utils.AddDiagnosticError(resp, + ErrUpdatingSubgraph, + fmt.Sprintf("Could not update subgraph '%s': %s", data.Name.ValueString(), apiErr.Error()), + ) + return } subgraph, err := r.client.GetSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString()) @@ -274,6 +256,36 @@ func (r *SubgraphResource) Update(ctx context.Context, req resource.UpdateReques ) return } + if data.Schema.ValueString() != "" { + apiResponse, apiError := r.client.PublishSubgraph(ctx, data.Name.ValueString(), data.Namespace.ValueString(), data.Schema.ValueString()) + if apiError != nil { + if api.IsSubgraphCompositionFailedError(apiError) { + utils.AddDiagnosticWarning(resp, + ErrPublishingSubgraph, + fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), apiError.Error()), + ) + } else if api.IsInvalidSubgraphSchemaError(apiError) { + utils.AddDiagnosticError(resp, + ErrPublishingSubgraph, + fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), apiError.Error()), + ) + return + } else { + utils.AddDiagnosticError(resp, + ErrPublishingSubgraph, + fmt.Sprintf("Could not publish subgraph '%s': %s", data.Name.ValueString(), apiError.Error()), + ) + return + } + } + + if apiResponse.HasChanged != nil && *apiResponse.HasChanged { + resp.Diagnostics.AddWarning( + ErrSubgraphSchemaChanged, + fmt.Sprintf("The schema for subgraph '%s' has changed and was published.", data.Name.ValueString()), + ) + } + } data.Id = types.StringValue(subgraph.GetId()) data.Name = types.StringValue(subgraph.GetName()) diff --git a/internal/service/subgraph/resource_cosmo_subgraph_test.go b/internal/service/subgraph/resource_cosmo_subgraph_test.go index 24ae641..df04a8a 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" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -20,12 +21,14 @@ func TestAccSubgraphResource(t *testing.T) { routingURL := "https://example.com" updatedRoutingURL := "https://updated-example.com" + subgraphSchema := acceptance.TestAccValidSubgraphSchema + resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), @@ -35,7 +38,7 @@ func TestAccSubgraphResource(t *testing.T) { ), }, { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, updatedRoutingURL), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, updatedRoutingURL, subgraphSchema), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "routing_url", updatedRoutingURL), ), @@ -45,7 +48,7 @@ func TestAccSubgraphResource(t *testing.T) { RefreshState: true, }, { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema), Destroy: true, }, }, @@ -62,13 +65,14 @@ func TestAccStandaloneSubgraphResource(t *testing.T) { subgraphName := acctest.RandomWithPrefix("test-subgraph") routingURL := "https://example.com" + subgraphSchema := acceptance.TestAccValidSubgraphSchema resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL), + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, routingURL, subgraphSchema), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), @@ -78,7 +82,7 @@ func TestAccStandaloneSubgraphResource(t *testing.T) { ), }, { - Config: testStandaloneSubgraph(namespace, subgraphName, routingURL), + Config: testStandaloneSubgraph(namespace, subgraphName, routingURL, subgraphSchema), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cosmo_subgraph.test", "name", subgraphName), resource.TestCheckResourceAttr("cosmo_subgraph.test", "namespace", namespace), @@ -92,14 +96,65 @@ func TestAccStandaloneSubgraphResource(t *testing.T) { RefreshState: true, }, { - Config: testStandaloneSubgraph(namespace, subgraphName, routingURL), + Config: testStandaloneSubgraph(namespace, subgraphName, routingURL, subgraphSchema), Destroy: true, }, }, }) } -func testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL string) string { +func TestAccSubgraphResourceInvalidSchema(t *testing.T) { + namespace := acctest.RandomWithPrefix("test-namespace") + subgraphName := acctest.RandomWithPrefix("test-subgraph") + subgraphRoutingURL := "https://example.com" + + federatedGraphName := acctest.RandomWithPrefix("test-subgraph") + federatedGraphRoutingURL := "https://example.com" + subgraphSchema := "invalid" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphRoutingURL, subgraphName, subgraphRoutingURL, subgraphSchema), + ExpectError: regexp.MustCompile(`.*ERR_INVALID_SUBGRAPH_SCHEMA*`), + }, + }, + }) +} + +func TestAccStandaloneSubgraphResourcePublishSchema(t *testing.T) { + namespace := acctest.RandomWithPrefix("test-namespace") + subgraphName := acctest.RandomWithPrefix("test-subgraph") + subgraphRoutingURL := "https://example.com" + + subgraphSchema := acceptance.TestAccValidSubgraphSchema + updatedSubgraphSchema := "invalid" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acceptance.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema), + 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", "labels.team", "backend"), + resource.TestCheckResourceAttr("cosmo_subgraph.test", "labels.stage", "dev"), + ), + }, + { + Config: testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, updatedSubgraphSchema), + ExpectError: regexp.MustCompile(`.*ERR_INVALID_SUBGRAPH_SCHEMA*`), + }, + }, + }) +} + +func testAccSubgraphResourceConfig(namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL, subgraphSchema string) string { return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" @@ -118,15 +173,18 @@ resource "cosmo_subgraph" "test" { name = "%s" namespace = cosmo_namespace.test.name routing_url = "%s" + schema = <<-EOT + %s + EOT labels = { "team" = "backend", "stage" = "dev" } } -`, namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL) +`, namespace, federatedGraphName, federatedGraphroutingURL, subgraphName, subgraphRoutingURL, subgraphSchema) } -func testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL string) string { +func testStandaloneSubgraph(namespace, subgraphName, subgraphRoutingURL, subgraphSchema string) string { return fmt.Sprintf(` resource "cosmo_namespace" "test" { name = "%s" @@ -136,10 +194,13 @@ resource "cosmo_subgraph" "test" { name = "%s" namespace = cosmo_namespace.test.name routing_url = "%s" + schema = <<-EOT + %s + EOT labels = { "team" = "backend", "stage" = "dev" } } -`, namespace, subgraphName, subgraphRoutingURL) +`, namespace, subgraphName, subgraphRoutingURL, subgraphSchema) } From 263811986361e8910f5cfa8d6f67ff63ec83c4b1 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 13:47:06 +0200 Subject: [PATCH 19/27] fix: golangci-lint issues --- .../resource_cosmo_federated_graph.go | 12 ++---------- .../subgraph/resource_cosmo_subgraph_test.go | 17 ++++++++--------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/internal/service/federated-graph/resource_cosmo_federated_graph.go b/internal/service/federated-graph/resource_cosmo_federated_graph.go index 524386c..980b4f7 100644 --- a/internal/service/federated-graph/resource_cosmo_federated_graph.go +++ b/internal/service/federated-graph/resource_cosmo_federated_graph.go @@ -137,13 +137,10 @@ 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.AddDiagnosticWarning(resp, ErrCompositionError, apiError.Error()) - } else { + if !api.IsSubgraphCompositionFailedError(apiError) { utils.AddDiagnosticError(resp, ErrCreatingGraph, apiError.Error()) return } - utils.AddDiagnosticWarning(resp, ErrCompositionError, apiError.Error()) } graph := response.Graph @@ -230,7 +227,7 @@ func (r *FederatedGraphResource) Update(ctx context.Context, req resource.Update admissionWebhookSecret = data.AdmissionWebhookSecret.ValueStringPointer() } - apiResponse, apiError := r.client.UpdateFederatedGraph(ctx, admissionWebhookSecret, &graph) + _, apiError := r.client.UpdateFederatedGraph(ctx, admissionWebhookSecret, &graph) if apiError != nil { if api.IsSubgraphCompositionFailedError(apiError) { utils.AddDiagnosticWarning(resp, @@ -246,11 +243,6 @@ func (r *FederatedGraphResource) Update(ctx context.Context, req resource.Update } } - if len(apiResponse.CompositionErrors) > 0 { - utils.AddDiagnosticWarning(resp, ErrCompositionError, fmt.Sprintf("Composition errors: %v, graph name: %s, graph namespace: %s", apiResponse.CompositionErrors, graph.GetName(), graph.GetNamespace())) - return - } - utils.LogAction(ctx, "updated", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/service/subgraph/resource_cosmo_subgraph_test.go b/internal/service/subgraph/resource_cosmo_subgraph_test.go index df04a8a..cf0950c 100644 --- a/internal/service/subgraph/resource_cosmo_subgraph_test.go +++ b/internal/service/subgraph/resource_cosmo_subgraph_test.go @@ -14,12 +14,12 @@ func TestAccSubgraphResource(t *testing.T) { namespace := acctest.RandomWithPrefix("test-namespace") federatedGraphName := acctest.RandomWithPrefix("test-subgraph") - federatedGraphRoutingURL := "https://example.com" + federatedGraphRoutingURL := "https://federated-graph-example.com" subgraphName := acctest.RandomWithPrefix("test-subgraph") - routingURL := "https://example.com" - updatedRoutingURL := "https://updated-example.com" + routingURL := "https://subgraph-example.com" + updatedRoutingURL := "https://updated-subgraph-example.com" subgraphSchema := acceptance.TestAccValidSubgraphSchema @@ -55,16 +55,15 @@ func TestAccSubgraphResource(t *testing.T) { }) } -// these subgraphs should be deleteable without any issues func TestAccStandaloneSubgraphResource(t *testing.T) { namespace := acctest.RandomWithPrefix("test-namespace") federatedGraphName := acctest.RandomWithPrefix("test-subgraph") - federatedGraphRoutingURL := "https://example.com" + federatedGraphRoutingURL := "https://federated-graph-standalone-subgraph-example.com" subgraphName := acctest.RandomWithPrefix("test-subgraph") - routingURL := "https://example.com" + routingURL := "https://subgraph-standalone-example.com" subgraphSchema := acceptance.TestAccValidSubgraphSchema resource.Test(t, resource.TestCase{ @@ -106,10 +105,10 @@ func TestAccStandaloneSubgraphResource(t *testing.T) { func TestAccSubgraphResourceInvalidSchema(t *testing.T) { namespace := acctest.RandomWithPrefix("test-namespace") subgraphName := acctest.RandomWithPrefix("test-subgraph") - subgraphRoutingURL := "https://example.com" + subgraphRoutingURL := "https://subgraph-invalid-schema-example.com" federatedGraphName := acctest.RandomWithPrefix("test-subgraph") - federatedGraphRoutingURL := "https://example.com" + federatedGraphRoutingURL := "https://federated-graph-invalid-subgraph-schema-example.com" subgraphSchema := "invalid" resource.ParallelTest(t, resource.TestCase{ @@ -127,7 +126,7 @@ func TestAccSubgraphResourceInvalidSchema(t *testing.T) { func TestAccStandaloneSubgraphResourcePublishSchema(t *testing.T) { namespace := acctest.RandomWithPrefix("test-namespace") subgraphName := acctest.RandomWithPrefix("test-subgraph") - subgraphRoutingURL := "https://example.com" + subgraphRoutingURL := "https://subgraph-publish-schema-example.com" subgraphSchema := acceptance.TestAccValidSubgraphSchema updatedSubgraphSchema := "invalid" From 2242c85ede96a88ddfcf74daa411872b72af1767 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 15:18:50 +0200 Subject: [PATCH 20/27] feat: make contract readable add guide --- examples/guides/cosmo-local/contract.tf | 12 ++++ examples/guides/cosmo-local/graphs.tf | 13 +++- examples/guides/cosmo-local/main.tf | 41 +++++++------ examples/guides/cosmo-local/outputs.tf | 8 +-- examples/guides/cosmo-local/variables.tf | 5 ++ .../guides/cosmo-monograph-contract/main.tf | 1 - examples/resources/cosmo_contract/resource.tf | 9 +-- .../resources/cosmo_contract/variables.tf | 4 ++ internal/api/contract.go | 2 +- .../contract/resource_cosmo_contract.go | 59 ++++++++++++++++--- modules/cosmo-federated-graph/outputs.tf | 8 +++ 11 files changed, 118 insertions(+), 44 deletions(-) create mode 100644 examples/guides/cosmo-local/contract.tf diff --git a/examples/guides/cosmo-local/contract.tf b/examples/guides/cosmo-local/contract.tf new file mode 100644 index 0000000..d0446a1 --- /dev/null +++ b/examples/guides/cosmo-local/contract.tf @@ -0,0 +1,12 @@ +module "cosmo_contract" { + # only for test purposes give each federated graph a contract + for_each = var.federated_graphs + source = "../../resources/cosmo_contract" + + name = "${each.value.name}-contract" + namespace = module.cosmo_federated_graph[each.key].namespace_name + source_graph_name = module.cosmo_federated_graph[each.key].federated_graph_name + routing_url = "http://localhost:3000" + exclude_tags = ["backend"] +} + diff --git a/examples/guides/cosmo-local/graphs.tf b/examples/guides/cosmo-local/graphs.tf index 47c43a0..f3ef398 100644 --- a/examples/guides/cosmo-local/graphs.tf +++ b/examples/guides/cosmo-local/graphs.tf @@ -10,6 +10,17 @@ resource "random_string" "module_prefix" { // namespace have to be lowercase therefore we lowercase the prefix locals { prefix = lower(random_string.module_prefix.result) + schema = < Date: Fri, 20 Sep 2024 15:19:47 +0200 Subject: [PATCH 21/27] chore: handle errors consistently --- docs/resources/contract.md | 9 +- examples/guides/cosmo-local/graphs.tf | 22 ++-- examples/guides/cosmo-local/variables.tf | 14 ++- internal/service/federated-graph/errors.go | 3 +- .../resource_cosmo_federated_graph.go | 29 +++-- internal/service/monograph/errors.go | 1 + .../monograph/resource_cosmo_monograph.go | 20 ++-- .../namespace/data_source_cosmo_namespace.go | 9 +- .../namespace/resource_cosmo_namespace.go | 25 +++- .../router-token/resource_cosmo_token.go | 7 +- .../subgraph/data_source_cosmo_subgraph.go | 2 +- internal/service/subgraph/errors.go | 21 ++-- .../subgraph/resource_cosmo_subgraph.go | 112 ++++++++++-------- 13 files changed, 168 insertions(+), 106 deletions(-) diff --git a/docs/resources/contract.md b/docs/resources/contract.md index a52356e..47e1e39 100644 --- a/docs/resources/contract.md +++ b/docs/resources/contract.md @@ -17,10 +17,11 @@ For more information, refer to the Cosmo Documentation at https://cosmo-docs.wun ```terraform resource "cosmo_contract" "test" { - name = var.name - namespace = var.namespace - source = var.source_graph_name - routing_url = var.routing_url + name = var.name + namespace = var.namespace + source = var.source_graph_name + routing_url = var.routing_url + exclude_tags = var.exclude_tags } ``` diff --git a/examples/guides/cosmo-local/graphs.tf b/examples/guides/cosmo-local/graphs.tf index f3ef398..47f0bb5 100644 --- a/examples/guides/cosmo-local/graphs.tf +++ b/examples/guides/cosmo-local/graphs.tf @@ -11,14 +11,13 @@ resource "random_string" "module_prefix" { locals { prefix = lower(random_string.module_prefix.result) schema = < Date: Fri, 20 Sep 2024 15:23:45 +0200 Subject: [PATCH 22/27] fix: reread label matchers in resource --- internal/service/contract/data_source_cosmo_contract.go | 8 ++++++++ internal/service/contract/resource_cosmo_contract.go | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/service/contract/data_source_cosmo_contract.go b/internal/service/contract/data_source_cosmo_contract.go index dd905d1..42ef513 100644 --- a/internal/service/contract/data_source_cosmo_contract.go +++ b/internal/service/contract/data_source_cosmo_contract.go @@ -3,6 +3,7 @@ package contract import ( "context" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -123,11 +124,18 @@ 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()) + labelMatchers := make(map[string]attr.Value) + for _, labelMatcher := range graph.GetLabelMatchers() { + labelMatchers[labelMatcher] = types.StringValue(labelMatcher) + } + data.LabelMatchers = types.MapValueMust(types.StringType, labelMatchers) + if graph.Readme != nil { data.Readme = types.StringValue(*graph.Readme) } diff --git a/internal/service/contract/resource_cosmo_contract.go b/internal/service/contract/resource_cosmo_contract.go index 68df3e0..1bab6a3 100644 --- a/internal/service/contract/resource_cosmo_contract.go +++ b/internal/service/contract/resource_cosmo_contract.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -171,12 +170,6 @@ func (r *contractResource) Read(ctx context.Context, req resource.ReadRequest, r data.Namespace = types.StringValue(graph.GetNamespace()) data.RoutingURL = types.StringValue(graph.GetRoutingURL()) - var excludeTags []attr.Value - for _, matcher := range graph.LabelMatchers { - excludeTags = append(excludeTags, types.StringValue(matcher)) - } - data.ExcludeTags = types.ListValueMust(types.StringType, excludeTags) - utils.LogAction(ctx, "read", data.Id.ValueString(), data.Name.ValueString(), data.Namespace.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) From ad8597f559c7f63b8368d50049a2909cb932c71a Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 15:37:08 +0200 Subject: [PATCH 23/27] fix: examples --- examples/guides/cosmo-monograph-contract/main.tf | 1 + examples/guides/cosmo-monograph-contract/variables.tf | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/examples/guides/cosmo-monograph-contract/main.tf b/examples/guides/cosmo-monograph-contract/main.tf index 9c53781..169320e 100644 --- a/examples/guides/cosmo-monograph-contract/main.tf +++ b/examples/guides/cosmo-monograph-contract/main.tf @@ -20,4 +20,5 @@ module "cosmo_contract" { namespace = module.cosmo_namespace.name source_graph_name = module.cosmo_monograph.name routing_url = var.contract_routing_url + exclude_tags = var.contract_exclude_tags } diff --git a/examples/guides/cosmo-monograph-contract/variables.tf b/examples/guides/cosmo-monograph-contract/variables.tf index 1d75275..b5e2980 100644 --- a/examples/guides/cosmo-monograph-contract/variables.tf +++ b/examples/guides/cosmo-monograph-contract/variables.tf @@ -29,6 +29,11 @@ variable "contract_routing_url" { default = "http://example.com/routing" } +variable "contract_exclude_tags" { + type = list(string) + default = [] +} + variable "api_url" { type = string default = "http://example.com/graphql" From 6997adbd67142c12435741f70bacc5af5e9874f1 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 15:37:22 +0200 Subject: [PATCH 24/27] chore: streamline error handling in contracts --- internal/service/contract/resource_cosmo_contract.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/contract/resource_cosmo_contract.go b/internal/service/contract/resource_cosmo_contract.go index 1bab6a3..d55d1ad 100644 --- a/internal/service/contract/resource_cosmo_contract.go +++ b/internal/service/contract/resource_cosmo_contract.go @@ -187,7 +187,7 @@ func (r *contractResource) Update(ctx context.Context, req resource.UpdateReques if err != nil { utils.AddDiagnosticError(resp, ErrUpdatingContract, - "Could not update contract: "+err.Error(), + err.Error(), ) return } @@ -224,18 +224,18 @@ func (r *contractResource) Delete(ctx context.Context, req resource.DeleteReques if api.IsContractCompositionFailedError(apiError) || api.IsSubgraphCompositionFailedError(apiError) { utils.AddDiagnosticWarning(resp, ErrDeletingContract, - "Contract composition failed: "+apiError.Error(), + apiError.Error(), ) } else if api.IsNotFoundError(apiError) { utils.AddDiagnosticWarning(resp, ErrDeletingContract, - "Contract composition failed: "+apiError.Error(), + apiError.Error(), ) resp.State.RemoveResource(ctx) } else { utils.AddDiagnosticError(resp, ErrDeletingContract, - "Could not delete contract: "+apiError.Error(), + apiError.Error(), ) return } From bf1303c33fbbd4fbe0b564ae9cf61a3af45801d8 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 15:39:16 +0200 Subject: [PATCH 25/27] fix: example --- examples/provider/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/provider/main.tf b/examples/provider/main.tf index 3ce510a..a773a23 100644 --- a/examples/provider/main.tf +++ b/examples/provider/main.tf @@ -35,6 +35,7 @@ module "resource_cosmo_contract" { namespace = module.resource_cosmo_namespace.name routing_url = module.resource_cosmo_federated_graph.routing_url source_graph_name = module.resource_cosmo_federated_graph.name + exclude_tags = ["backend"] } module "data_cosmo_federated_graph" { From 9db0b8b1f9217d5e37b915e01e98e8edbef52ce5 Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Fri, 20 Sep 2024 15:48:12 +0200 Subject: [PATCH 26/27] docs: update readme --- README.md | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 103f4e5..bc5d8ea 100644 --- a/README.md +++ b/README.md @@ -203,21 +203,42 @@ The following commands are used to build and install the provider binary locally The Makefile includes several tasks to facilitate development and testing. For local development, `make build install` should be used to install the provider locally. +### General Build Tasks + - **default**: Runs acceptance tests. - **testacc**: Runs tests with a timeout. +- **test-go**: Runs Go tests. +- **test**: Cleans, builds, installs, runs acceptance tests, and executes end-to-end tests. - **generate**: Updates generated files. - **tidy**: Cleans up the `go.mod` file. -- **fmt**: Formats code. +- **fmt**: Formats Go and Terraform files for consistency. - **build**: Compiles the provider binary. - **install**: Installs the binary in the Terraform plugin directory after building it. +- **clean-local**: Cleans up local build artifacts. - **build-all-arches**: Compiles the binary for multiple OS and architectures. -- **release**: Generates files and builds binaries for all architectures. -- **e2e-cd-apply**: Runs end-to-end tests for apply. (References: `examples/provider`) -- **e2e-cd-destroy**: Runs end-to-end tests for destroy. (References: `examples/provider`) -- **e2e-cosmo-apply**: Runs end-to-end tests for the cosmo feature. (References: `examples/cosmo`) -- **e2e-cosmo-destroy**: Runs end-to-end tests for cosmo destroy. (References: `examples/cosmo`) -- **e2e-cosmo-monograph-apply**: Runs end-to-end tests for the monograph feature. (References: `examples/resources/comso_monograph`) -- **e2e-cosmo-monograph-destroy**: Runs end-to-end tests for monograph destroy. (References: `examples/resources/comso_monograph`) -- **e2e-apply-cosmo-local**: Runs end-to-end tests for cosmo local. (References: `examples/cosmo-local`) -- **e2e-destroy-cosmo-local**: Runs end-to-end tests for cosmo local destroy. (References: `examples/cosmo-local`) -- **e2e-clean-cosmo-local**: Cleans up the cosmo local setup. (References: `examples/cosmo-local`) + +### End-to-End (E2E) Tasks + +- **e2e-apply-cd**: Runs end-to-end tests for the CD feature (points to `examples/provider`). +- **e2e-destroy-cd**: Cleans up after CD tests (points to `examples/provider`). +- **e2e-clean-cd**: Cleans up CD test artifacts (points to `examples/provider`). +- **e2e-apply-cosmo**: Runs end-to-end tests for the Cosmo feature (points to `examples/guides/cosmo`). +- **e2e-destroy-cosmo**: Cleans up after Cosmo tests (points to `examples/guides/cosmo`). +- **e2e-clean-cosmo**: Cleans up Cosmo test artifacts (points to `examples/guides/cosmo`). +- **e2e-apply-cosmo-monograph**: Runs end-to-end tests for the Cosmo monograph feature (points to `examples/guides/cosmo-monograph`). +- **e2e-destroy-cosmo-monograph**: Cleans up after Cosmo monograph tests (points to `examples/guides/cosmo-monograph`). +- **e2e-clean-cosmo-monograph**: Cleans up Cosmo monograph test artifacts (points to `examples/guides/cosmo-monograph`). +- **e2e-apply-cosmo-monograph-contract**: Runs end-to-end tests for the Cosmo monograph contract feature (points to `examples/guides/cosmo-monograph-contract`). +- **e2e-destroy-cosmo-monograph-contract**: Cleans up after Cosmo monograph contract tests (points to `examples/guides/cosmo-monograph-contract`). +- **e2e-clean-cosmo-monograph-contract**: Cleans up Cosmo monograph contract test artifacts (points to `examples/guides/cosmo-monograph-contract`). +- **e2e-apply-cosmo-local**: Runs end-to-end tests for the local Cosmo setup (points to `examples/guides/cosmo-local`). +- **e2e-destroy-cosmo-local**: Cleans up after local Cosmo tests (points to `examples/guides/cosmo-local`). +- **e2e-clean-cosmo-local**: Cleans up local Cosmo test artifacts (points to `examples/guides/cosmo-local`). +- **e2e-cd**: Runs both apply and destroy for CD tests. +- **e2e-cosmo**: Runs both apply and destroy for Cosmo tests. +- **e2e-cosmo-monograph**: Runs both apply and destroy for Cosmo monograph tests. +- **e2e-cosmo-monograph-contract**: Runs both apply and destroy for Cosmo monograph contract tests. +- **e2e-cosmo-local**: Runs both apply and destroy for local Cosmo tests. +- **e2e**: Runs all end-to-end tests. +- **clean**: Cleans up all test artifacts and local builds. +- **destroy**: Cleans up all resources created by the tests. From a249e1e71865ecb92911ee90a9a4d49403741c6b Mon Sep 17 00:00:00 2001 From: Andreas Zeissner Date: Wed, 25 Sep 2024 20:26:24 +0200 Subject: [PATCH 27/27] docs: add missing resources --- docs/index.md | 6 ++---- internal/provider/provider.go | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index fdd875a..26f6392 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,16 +4,14 @@ page_title: "cosmo Provider" subcategory: "" description: |- The Cosmo provider allows you to interact with WunderGraph's Cosmo API, managing key resources. - It supports creating and reading namespaces, federated graphs, and monographs. - You can also generate router tokens for use with the router, and manage subgraphs by creating, publishing, or reading them. + It supports creating and reading namespaces, federated graphs, subgraphs, router tokens, monographs, and contracts. Refer to the official Cosmo Documentation https://cosmo-docs.wundergraph.com/ for more details. --- # cosmo Provider The Cosmo provider allows you to interact with WunderGraph's Cosmo API, managing key resources. -It supports creating and reading namespaces, federated graphs, and monographs. -You can also generate router tokens for use with the router, and manage subgraphs by creating, publishing, or reading them. +It supports creating and reading namespaces, federated graphs, subgraphs, router tokens, monographs, and contracts. Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) for more details. diff --git a/internal/provider/provider.go b/internal/provider/provider.go index becdde9..75cebdd 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -52,8 +52,7 @@ func (p *CosmoProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp.Schema = schema.Schema{ MarkdownDescription: ` The Cosmo provider allows you to interact with WunderGraph's Cosmo API, managing key resources. -It supports creating and reading namespaces, federated graphs, and monographs. -You can also generate router tokens for use with the router, and manage subgraphs by creating, publishing, or reading them. +It supports creating and reading namespaces, federated graphs, subgraphs, router tokens, monographs, and contracts. Refer to the official [Cosmo Documentation](https://cosmo-docs.wundergraph.com/) for more details. `,