From 5baef8c6e9b38a586061d7f4f6151071f9e1545b Mon Sep 17 00:00:00 2001 From: Lukas W Date: Tue, 1 Nov 2022 13:18:26 +0100 Subject: [PATCH 1/7] Update terraform-plugin-framework --- go.mod | 2 +- go.sum | 8 ++ internal/provider/app_data_source.go | 25 ++-- internal/provider/app_resource.go | 38 +++--- internal/provider/certificate_data_source.go | 24 ++-- internal/provider/certificate_resource.go | 37 +++--- internal/provider/ip_data_source.go | 24 ++-- internal/provider/ip_resource.go | 34 +++--- internal/provider/machine_resource.go | 40 +++---- internal/provider/models.go | 30 ++++- internal/provider/provider.go | 116 +++++++++---------- internal/provider/volume_data_source.go | 23 ++-- internal/provider/volume_resource.go | 34 +++--- 13 files changed, 206 insertions(+), 229 deletions(-) diff --git a/go.mod b/go.mod index 016d89c..cca26f4 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/Khan/genqlient v0.5.0 github.com/google/uuid v1.2.0 - github.com/hashicorp/terraform-plugin-framework v0.11.1 + github.com/hashicorp/terraform-plugin-framework v0.14.0 github.com/hashicorp/terraform-plugin-go v0.14.0 github.com/hashicorp/terraform-plugin-log v0.7.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.22.0 diff --git a/go.sum b/go.sum index 399bfa2..1b1ae7e 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,14 @@ github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= github.com/hashicorp/terraform-plugin-framework v0.11.1 h1:rq8f+TLDO4tJu+n9mMYlDrcRoIdrg0gTUvV2Jr0Ya24= github.com/hashicorp/terraform-plugin-framework v0.11.1/go.mod h1:GENReHOz6GEt8Jk3UN94vk8BdC6irEHFgN3Z9HPhPUU= +github.com/hashicorp/terraform-plugin-framework v0.12.0 h1:Bk3l5MQUaZoo5eplr+u1FomYqGS564e8Tp3rutnCfYg= +github.com/hashicorp/terraform-plugin-framework v0.12.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= +github.com/hashicorp/terraform-plugin-framework v0.13.0 h1:tGnqttzZwU3FKc+HasHr2Yi5L81FcQbdc8zQhbBD9jQ= +github.com/hashicorp/terraform-plugin-framework v0.13.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= +github.com/hashicorp/terraform-plugin-framework v0.14.0 h1:Mwj55u+Jc/QGM6fLBPCe1P+ZF3cuYs6wbCdB15lx/Dg= +github.com/hashicorp/terraform-plugin-framework v0.14.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= +github.com/hashicorp/terraform-plugin-framework v0.15.0 h1:6f4UY2yfp5UsSX9JhUA6RSptjd+ojStBGWA4jrPhB6Q= +github.com/hashicorp/terraform-plugin-framework v0.15.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= github.com/hashicorp/terraform-plugin-go v0.14.0 h1:ttnSlS8bz3ZPYbMb84DpcPhY4F5DsQtcAS7cHo8uvP4= github.com/hashicorp/terraform-plugin-go v0.14.0/go.mod h1:2nNCBeRLaenyQEi78xrGrs9hMbulveqG/zDMQSvVJTE= github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs= diff --git a/internal/provider/app_data_source.go b/internal/provider/app_data_source.go index 3926d5f..3536774 100644 --- a/internal/provider/app_data_source.go +++ b/internal/provider/app_data_source.go @@ -2,21 +2,14 @@ package provider import ( "context" - "github.com/fly-apps/terraform-provider-fly/graphql" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" ) -// Ensure provider defined types fully satisfy framework interfaces -var _ tfsdkprovider.DataSourceType = appDataSourceType{} -var _ datasource.DataSource = appDataSource{} - -type appDataSourceType struct{} +var _ datasource.DataSourceWithConfigure = &appDataSource{} // Matches getSchema type appDataSourceOutput struct { @@ -32,7 +25,11 @@ type appDataSourceOutput struct { //Secrets types.Map `tfsdk:"secrets"` } -func (a appDataSourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (d appDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "fly_app" +} + +func (appDataSource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Retrieve info about graphql app", @@ -78,12 +75,8 @@ func (a appDataSourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Di }, nil } -func (a appDataSourceType) NewDataSource(ctx context.Context, in tfsdkprovider.Provider) (datasource.DataSource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return appDataSource{ - provider: provider, - }, diags +func newAppDataSource() datasource.DataSource { + return &appDataSource{} } func (d appDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { @@ -98,7 +91,7 @@ func (d appDataSource) Read(ctx context.Context, req datasource.ReadRequest, res appName := data.Name.Value - queryresp, err := graphql.GetFullApp(context.Background(), *d.provider.client, appName) + queryresp, err := graphql.GetFullApp(context.Background(), d.gqlClient, appName) if err != nil { resp.Diagnostics.AddError("Query failed", err.Error()) } diff --git a/internal/provider/app_resource.go b/internal/provider/app_resource.go index 3528098..e305f68 100644 --- a/internal/provider/app_resource.go +++ b/internal/provider/app_resource.go @@ -8,7 +8,6 @@ import ( "github.com/fly-apps/terraform-provider-fly/internal/utils" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -16,11 +15,8 @@ import ( "github.com/vektah/gqlparser/v2/gqlerror" ) -var _ tfsdkprovider.ResourceType = flyAppResourceType{} -var _ resource.Resource = flyAppResource{} -var _ resource.ResourceWithImportState = flyAppResource{} - -type flyAppResourceType struct{} +var _ resource.ResourceWithConfigure = &flyAppResource{} +var _ resource.ResourceWithImportState = &flyAppResource{} type flyAppResourceData struct { Name types.String `tfsdk:"name"` @@ -31,7 +27,11 @@ type flyAppResourceData struct { //Secrets types.Map `tfsdk:"secrets"` } -func (ar flyAppResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (r flyAppResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "fly_app" +} + +func (r flyAppResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Fly app resource", @@ -73,16 +73,12 @@ func (ar flyAppResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diag }, nil } -func (ar flyAppResourceType) NewResource(_ context.Context, in tfsdkprovider.Provider) (resource.Resource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return flyAppResource{ - provider: provider, - }, diags +func newAppResource() resource.Resource { + return &flyAppResource{} } type flyAppResource struct { - provider provider + flyResource } func (r flyAppResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -96,7 +92,7 @@ func (r flyAppResource) Create(ctx context.Context, req resource.CreateRequest, } if data.Org.Unknown { - defaultOrg, err := utils.GetDefaultOrg(*r.provider.client) + defaultOrg, err := utils.GetDefaultOrg(r.gqlClient) if err != nil { resp.Diagnostics.AddError("Could not detect default organization", err.Error()) return @@ -104,14 +100,14 @@ func (r flyAppResource) Create(ctx context.Context, req resource.CreateRequest, data.OrgId.Value = defaultOrg.Id data.Org.Value = defaultOrg.Name } else { - org, err := graphql.Organization(context.Background(), *r.provider.client, data.Org.Value) + org, err := graphql.Organization(context.Background(), r.gqlClient, data.Org.Value) if err != nil { resp.Diagnostics.AddError("Could not resolve organization", err.Error()) return } data.OrgId.Value = org.Organization.Id } - mresp, err := graphql.CreateAppMutation(context.Background(), *r.provider.client, data.Name.Value, data.OrgId.Value) + mresp, err := graphql.CreateAppMutation(context.Background(), r.gqlClient, data.Name.Value, data.OrgId.Value) if err != nil { resp.Diagnostics.AddError("Create app failed", err.Error()) return @@ -135,7 +131,7 @@ func (r flyAppResource) Create(ctx context.Context, req resource.CreateRequest, // Value: v, // }) // } - // _, err := graphql.SetSecrets(context.Background(), *r.provider.client, graphql.SetSecretsInput{ + // _, err := graphql.SetSecrets(context.Background(), *r.gqlClient, graphql.SetSecretsInput{ // AppId: data.Id.Value, // Secrets: secrets, // ReplaceAll: true, @@ -164,7 +160,7 @@ func (r flyAppResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - query, err := graphql.GetFullApp(context.Background(), *r.provider.client, state.Name.Value) + query, err := graphql.GetFullApp(context.Background(), r.gqlClient, state.Name.Value) var errList gqlerror.List if errors.As(err, &errList) { for _, err := range errList { @@ -229,7 +225,7 @@ func (r flyAppResource) Update(ctx context.Context, req resource.UpdateRequest, // Value: v, // }) // } - // _, err := graphql.SetSecrets(context.Background(), *r.provider.client, graphql.SetSecretsInput{ + // _, err := graphql.SetSecrets(context.Background(), r.gqlClient, graphql.SetSecretsInput{ // AppId: state.Id.Value, // Secrets: secrets, // ReplaceAll: true, @@ -254,7 +250,7 @@ func (r flyAppResource) Delete(ctx context.Context, req resource.DeleteRequest, diags := req.State.Get(ctx, &data) resp.Diagnostics.Append(diags...) - _, err := graphql.DeleteAppMutation(context.Background(), *r.provider.client, data.Name.Value) + _, err := graphql.DeleteAppMutation(context.Background(), r.gqlClient, data.Name.Value) var errList gqlerror.List if errors.As(err, &errList) { for _, err := range errList { diff --git a/internal/provider/certificate_data_source.go b/internal/provider/certificate_data_source.go index ee620a2..55d024b 100644 --- a/internal/provider/certificate_data_source.go +++ b/internal/provider/certificate_data_source.go @@ -9,15 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/vektah/gqlparser/v2/gqlerror" - - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" ) -// Ensure provider defined types fully satisfy framework interfaces -var _ tfsdkprovider.DataSourceType = certDataSourceType{} -var _ datasource.DataSource = certDataSource{} - -type certDataSourceType struct{} +var _ datasource.DataSourceWithConfigure = &certDataSource{} // Matches getSchema type certDataSourceOutput struct { @@ -30,7 +24,11 @@ type certDataSourceOutput struct { Check types.Bool `tfsdk:"check"` } -func (t certDataSourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (d certDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "fly_cert" +} + +func (d certDataSource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly certificate data source", Attributes: map[string]tfsdk.Attribute{ @@ -73,12 +71,8 @@ func (t certDataSourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.D }, nil } -func (t certDataSourceType) NewDataSource(ctx context.Context, in tfsdkprovider.Provider) (datasource.DataSource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return certDataSource{ - provider: provider, - }, diags +func newCertDataSource() datasource.DataSource { + return &certDataSource{} } func (d certDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { @@ -94,7 +88,7 @@ func (d certDataSource) Read(ctx context.Context, req datasource.ReadRequest, re hostname := data.Hostname.Value app := data.Appid.Value - query, err := graphql.GetCertificate(context.Background(), *d.provider.client, app, hostname) + query, err := graphql.GetCertificate(context.Background(), d.gqlClient, app, hostname) var errList gqlerror.List if errors.As(err, &errList) { for _, err := range errList { diff --git a/internal/provider/certificate_resource.go b/internal/provider/certificate_resource.go index 3f134ce..3acb2f9 100644 --- a/internal/provider/certificate_resource.go +++ b/internal/provider/certificate_resource.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "github.com/fly-apps/terraform-provider-fly/graphql" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -13,18 +12,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vektah/gqlparser/v2/gqlerror" - - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" + "strings" ) -var _ tfsdkprovider.ResourceType = flyCertResourceType{} -var _ resource.Resource = flyCertResource{} -var _ resource.ResourceWithImportState = flyCertResource{} - -type flyCertResourceType struct{} +var _ resource.ResourceWithConfigure = &flyCertResource{} +var _ resource.ResourceWithImportState = &flyCertResource{} type flyCertResource struct { - provider provider + flyResource +} + +func newFlyCertResource() resource.Resource { + return &flyCertResource{} } type flyCertResourceData struct { @@ -37,7 +36,11 @@ type flyCertResourceData struct { Check types.Bool `tfsdk:"check"` } -func (t flyCertResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (cr flyCertResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "fly_cert" +} + +func (flyCertResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly certificate resource", Attributes: map[string]tfsdk.Attribute{ @@ -80,21 +83,13 @@ func (t flyCertResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diag }, nil } -func (t flyCertResourceType) NewResource(ctx context.Context, in tfsdkprovider.Provider) (resource.Resource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return flyCertResource{ - provider: provider, - }, diags -} - func (cr flyCertResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data flyCertResourceData diags := req.Plan.Get(ctx, &data) resp.Diagnostics.Append(diags...) - q, err := graphql.AddCertificate(context.Background(), *cr.provider.client, data.Appid.Value, data.Hostname.Value) + q, err := graphql.AddCertificate(context.Background(), cr.gqlClient, data.Appid.Value, data.Hostname.Value) if err != nil { resp.Diagnostics.AddError("Failed to create cert", err.Error()) } @@ -127,7 +122,7 @@ func (cr flyCertResource) Read(ctx context.Context, req resource.ReadRequest, re hostname := data.Hostname.Value app := data.Appid.Value - query, err := graphql.GetCertificate(context.Background(), *cr.provider.client, app, hostname) + query, err := graphql.GetCertificate(context.Background(), cr.gqlClient, app, hostname) var errList gqlerror.List if errors.As(err, &errList) { for _, err := range errList { @@ -168,7 +163,7 @@ func (cr flyCertResource) Delete(ctx context.Context, req resource.DeleteRequest diags := req.State.Get(ctx, &data) resp.Diagnostics.Append(diags...) - _, err := graphql.DeleteCertificate(context.Background(), *cr.provider.client, data.Appid.Value, data.Hostname.Value) + _, err := graphql.DeleteCertificate(context.Background(), cr.gqlClient, data.Appid.Value, data.Hostname.Value) if err != nil { resp.Diagnostics.AddError("Delete cert failed", err.Error()) } diff --git a/internal/provider/ip_data_source.go b/internal/provider/ip_data_source.go index d90b44f..5b47996 100644 --- a/internal/provider/ip_data_source.go +++ b/internal/provider/ip_data_source.go @@ -12,15 +12,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vektah/gqlparser/v2/gqlerror" - - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" ) -// Ensure provider defined types fully satisfy framework interfaces -var _ tfsdkprovider.DataSourceType = ipDataSourceType{} -var _ datasource.DataSource = ipDataSource{} - -type ipDataSourceType struct{} +var _ datasource.DataSourceWithConfigure = &ipDataSource{} // Matches getSchema type ipDataSourceOutput struct { @@ -31,7 +25,11 @@ type ipDataSourceOutput struct { Type types.String `tfsdk:"type"` } -func (i ipDataSourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (i ipDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "fly_ip" +} + +func (i ipDataSource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly ip data source", Attributes: map[string]tfsdk.Attribute{ @@ -67,12 +65,8 @@ func (i ipDataSourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Dia }, nil } -func (i ipDataSourceType) NewDataSource(ctx context.Context, in tfsdkprovider.Provider) (datasource.DataSource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return ipDataSource{ - provider: provider, - }, diags +func newIpDataSource() datasource.DataSource { + return &ipDataSource{} } func (i ipDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { @@ -88,7 +82,7 @@ func (i ipDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp addr := data.Address.Value app := data.Appid.Value - query, err := graphql.IpAddressQuery(context.Background(), *i.provider.client, app, addr) + query, err := graphql.IpAddressQuery(context.Background(), i.gqlClient, app, addr) tflog.Info(ctx, fmt.Sprintf("Query res: for %s %s %+v", app, addr, query)) var errList gqlerror.List if errors.As(err, &errList) { diff --git a/internal/provider/ip_resource.go b/internal/provider/ip_resource.go index d8429fb..ab263be 100644 --- a/internal/provider/ip_resource.go +++ b/internal/provider/ip_resource.go @@ -8,7 +8,6 @@ import ( "github.com/fly-apps/terraform-provider-fly/internal/provider/modifiers" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -16,14 +15,15 @@ import ( "github.com/vektah/gqlparser/v2/gqlerror" ) -var _ tfsdkprovider.ResourceType = flyIpResourceType{} -var _ resource.Resource = flyIpResource{} -var _ resource.ResourceWithImportState = flyIpResource{} - -type flyIpResourceType struct{} +var _ resource.ResourceWithConfigure = &flyIpResource{} +var _ resource.ResourceWithImportState = &flyIpResource{} type flyIpResource struct { - provider provider + flyResource +} + +func newFlyIpResource() resource.Resource { + return &flyIpResource{} } type flyIpResourceData struct { @@ -34,7 +34,11 @@ type flyIpResourceData struct { Type types.String `tfsdk:"type"` } -func (t flyIpResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (ir flyIpResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "fly_ip" +} + +func (flyIpResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly ip resource", Attributes: map[string]tfsdk.Attribute{ @@ -70,14 +74,6 @@ func (t flyIpResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagno }, nil } -func (t flyIpResourceType) NewResource(ctx context.Context, in tfsdkprovider.Provider) (resource.Resource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return flyIpResource{ - provider: provider, - }, diags -} - func (ir flyIpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data flyIpResourceData @@ -86,7 +82,7 @@ func (ir flyIpResource) Create(ctx context.Context, req resource.CreateRequest, tflog.Info(ctx, fmt.Sprintf("%+v", data)) - q, err := graphql.AllocateIpAddress(context.Background(), *ir.provider.client, data.Appid.Value, data.Region.Value, graphql.IPAddressType(data.Type.Value)) + q, err := graphql.AllocateIpAddress(context.Background(), ir.gqlClient, data.Appid.Value, data.Region.Value, graphql.IPAddressType(data.Type.Value)) tflog.Info(ctx, fmt.Sprintf("query res in create ip: %+v", q)) if err != nil { resp.Diagnostics.AddError("Failed to create ip addr", err.Error()) @@ -118,7 +114,7 @@ func (ir flyIpResource) Read(ctx context.Context, req resource.ReadRequest, resp addr := data.Address.Value app := data.Appid.Value - query, err := graphql.IpAddressQuery(context.Background(), *ir.provider.client, app, addr) + query, err := graphql.IpAddressQuery(context.Background(), ir.gqlClient, app, addr) tflog.Info(ctx, fmt.Sprintf("Query res: for %s %s %+v", app, addr, query)) var errList gqlerror.List if errors.As(err, &errList) { @@ -160,7 +156,7 @@ func (ir flyIpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp.Diagnostics.Append(diags...) if !data.Id.Unknown && !data.Id.Null && data.Id.Value != "" { - _, err := graphql.ReleaseIpAddress(context.Background(), *ir.provider.client, data.Id.Value) + _, err := graphql.ReleaseIpAddress(context.Background(), ir.gqlClient, data.Id.Value) if err != nil { resp.Diagnostics.AddError("Release ip failed", err.Error()) } diff --git a/internal/provider/machine_resource.go b/internal/provider/machine_resource.go index 83b4dd9..1bf20ef 100644 --- a/internal/provider/machine_resource.go +++ b/internal/provider/machine_resource.go @@ -12,19 +12,17 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" ) -var _ tfsdkprovider.ResourceType = flyMachineResourceType{} -var _ resource.Resource = flyMachineResource{} -var _ resource.ResourceWithImportState = flyMachineResource{} - -type flyMachineResourceType struct{} +var _ resource.ResourceWithConfigure = &flyMachineResource{} +var _ resource.ResourceWithImportState = &flyMachineResource{} type flyMachineResource struct { - provider provider - endpoint string + flyResource +} + +func newFlyMachineResource() resource.Resource { + return &flyMachineResource{} } type TfPort struct { @@ -64,7 +62,11 @@ type TfMachineMount struct { Volume types.String `tfsdk:"volume"` } -func (mr flyMachineResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (mr flyMachineResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "fly_machine" +} + +func (flyMachineResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly machine resource", Attributes: map[string]tfsdk.Attribute{ @@ -209,16 +211,8 @@ func (mr flyMachineResourceType) GetSchema(context.Context) (tfsdk.Schema, diag. }, nil } -func (mr flyMachineResourceType) NewResource(_ context.Context, in tfsdkprovider.Provider) (resource.Resource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return flyMachineResource{ - provider: provider, - }, diags -} - func (mr flyMachineResource) ValidateOpenTunnel() (bool, error) { - _, err := mr.provider.httpClient.R().Get(fmt.Sprintf("http://%s", mr.provider.httpEndpoint)) + _, err := mr.httpClient.R().Get(fmt.Sprintf("http://%s", mr.httpEndpoint)) if err != nil { return false, errors.New("can't connect to the api, is the tunnel open? :)") } @@ -329,7 +323,7 @@ func (mr flyMachineResource) Create(ctx context.Context, req resource.CreateRequ createReq.Config.Mounts = mounts } - machineAPI := apiv1.NewMachineAPI(mr.provider.httpClient, mr.provider.httpEndpoint) + machineAPI := apiv1.NewMachineAPI(&mr.httpClient, mr.httpEndpoint) var newMachine apiv1.MachineResponse err = machineAPI.CreateMachine(createReq, data.App.Value, &newMachine) @@ -402,7 +396,7 @@ func (mr flyMachineResource) Read(ctx context.Context, req resource.ReadRequest, diags := req.State.Get(ctx, &data) resp.Diagnostics.Append(diags...) - machineAPI := apiv1.NewMachineAPI(mr.provider.httpClient, mr.provider.httpEndpoint) + machineAPI := apiv1.NewMachineAPI(&mr.httpClient, mr.httpEndpoint) var machine apiv1.MachineResponse @@ -538,7 +532,7 @@ func (mr flyMachineResource) Update(ctx context.Context, req resource.UpdateRequ updateReq.Config.Mounts = mounts } - machineApi := apiv1.NewMachineAPI(mr.provider.httpClient, mr.provider.httpEndpoint) + machineApi := apiv1.NewMachineAPI(&mr.httpClient, mr.httpEndpoint) var updatedMachine apiv1.MachineResponse @@ -604,7 +598,7 @@ func (mr flyMachineResource) Delete(ctx context.Context, req resource.DeleteRequ resp.Diagnostics.AddError("fly wireguard tunnel must be open", err.Error()) } - machineApi := apiv1.NewMachineAPI(mr.provider.httpClient, mr.provider.httpEndpoint) + machineApi := apiv1.NewMachineAPI(&mr.httpClient, mr.httpEndpoint) err = machineApi.DeleteMachine(data.App.Value, data.Id.Value, 50) diff --git a/internal/provider/models.go b/internal/provider/models.go index d18ef89..3c0d6b6 100644 --- a/internal/provider/models.go +++ b/internal/provider/models.go @@ -1,14 +1,36 @@ package provider +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +type flyDataSource struct { + providerClients +} + +func (d *flyDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.configure(req.ProviderData, &resp.Diagnostics) +} + +type flyResource struct { + providerClients +} + +func (r *flyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.configure(req.ProviderData, &resp.Diagnostics) +} + type appDataSource struct { - provider provider + flyDataSource } type certDataSource struct { - provider provider + flyDataSource } type ipDataSource struct { - provider provider + flyDataSource } type volumeDataSource struct { - provider provider + flyDataSource } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d83cf06..a27df5d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,6 +7,8 @@ import ( providerGraphql "github.com/fly-apps/terraform-provider-fly/graphql" "github.com/fly-apps/terraform-provider-fly/internal/utils" "github.com/fly-apps/terraform-provider-fly/internal/wg" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" hreq "github.com/imroc/req/v3" "net/http" "os" @@ -20,13 +22,32 @@ import ( var _ tfsdkprovider.Provider = &provider{} +type gqlClient graphql.Client + type provider struct { - configured bool - version string - token string + version string + token string +} + +type providerClients struct { httpEndpoint string - client *graphql.Client - httpClient *hreq.Client + gqlClient gqlClient + httpClient hreq.Client +} + +func (c *providerClients) configure(providerData any, diags *diag.Diagnostics) { + if providerData == nil { + return + } + + if p, ok := providerData.(*providerClients); ok { + *c = *p + } else { + diags.AddError( + "Unexpected Provider Instance Type", + fmt.Sprintf("While creating the data source or resource, an unexpected clients type (%T) was received. This is always a bug in the clients code and should be reported to the clients developers.", p), + ) + } } type providerData struct { @@ -49,7 +70,7 @@ func (p *provider) Configure(ctx context.Context, req tfsdkprovider.ConfigureReq var token string if data.FlyToken.Unknown { resp.Diagnostics.AddWarning( - "Unable to create client", + "Unable to create gqlClient", "Cannot use unknown value as token", ) return @@ -77,7 +98,8 @@ func (p *provider) Configure(ctx context.Context, req tfsdkprovider.ConfigureReq httpEndpoint = endpoint } - p.httpEndpoint = httpEndpoint + var clients providerClients + clients.httpEndpoint = httpEndpoint enableTracing := false _, ok := os.LookupEnv("DEBUG") @@ -86,20 +108,20 @@ func (p *provider) Configure(ctx context.Context, req tfsdkprovider.ConfigureReq resp.Diagnostics.AddWarning("Debug mode enabled", "Debug mode enabled, this will add the Fly-Force-Trace header to all graphql requests") } - p.httpClient = hreq.C() + clients.httpClient = *hreq.C() if enableTracing { - p.httpClient.SetCommonHeader("Fly-Force-Trace", "true") - p.httpClient = hreq.C().DevMode() + clients.httpClient.SetCommonHeader("Fly-Force-Trace", "true") + clients.httpClient = *hreq.C().DevMode() } - p.httpClient.SetCommonHeader("Authorization", "Bearer "+p.token) - p.httpClient.SetTimeout(2 * time.Minute) + clients.httpClient.SetCommonHeader("Authorization", "Bearer "+p.token) + clients.httpClient.SetTimeout(2 * time.Minute) // TODO: Make timeout configurable h := http.Client{Timeout: 60 * time.Second, Transport: &utils.Transport{UnderlyingTransport: http.DefaultTransport, Token: token, Ctx: ctx, EnableDebugTrace: enableTracing}} client := graphql.NewClient("https://api.fly.io/graphql", &h) - p.client = &client + clients.gqlClient = *(*gqlClient)(&client) if data.UseInternalTunnel.Value { org, err := providerGraphql.Organization(context.Background(), client, data.InternalTunnelOrg.Value) @@ -112,29 +134,30 @@ func (p *provider) Configure(ctx context.Context, req tfsdkprovider.ConfigureReq resp.Diagnostics.AddError("failed to open internal tunnel", err.Error()) return } - p.httpClient.SetDial(tunnel.NetStack().DialContext) - p.httpEndpoint = "_api.internal:4280" + clients.httpClient.SetDial(tunnel.NetStack().DialContext) + clients.httpEndpoint = "_api.internal:4280" } - p.configured = true -} -func (p *provider) GetResources(ctx context.Context) (map[string]tfsdkprovider.ResourceType, diag.Diagnostics) { + resp.ResourceData = &clients + resp.DataSourceData = &clients +} - return map[string]tfsdkprovider.ResourceType{ - "fly_app": flyAppResourceType{}, - "fly_volume": flyVolumeResourceType{}, - "fly_ip": flyIpResourceType{}, - "fly_cert": flyCertResourceType{}, - "fly_machine": flyMachineResourceType{}, - }, nil +func (p *provider) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + newAppResource, + newFlyVolumeResource, + newFlyIpResource, + newFlyCertResource, + newFlyMachineResource, + } } -func (p *provider) GetDataSources(ctx context.Context) (map[string]tfsdkprovider.DataSourceType, diag.Diagnostics) { - return map[string]tfsdkprovider.DataSourceType{ - "fly_app": appDataSourceType{}, - "fly_cert": certDataSourceType{}, - "fly_ip": ipDataSourceType{}, - }, nil +func (p *provider) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + newAppDataSource, + newCertDataSource, + newIpDataSource, + } } func (p *provider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { @@ -146,7 +169,7 @@ func (p *provider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostic Type: types.StringType, }, "fly_http_endpoint": { - MarkdownDescription: "Where the provider should look to find the fly http endpoint", + MarkdownDescription: "Where the clients should look to find the fly http endpoint", Optional: true, Type: types.StringType, }, @@ -173,32 +196,3 @@ func New(version string) func() tfsdkprovider.Provider { } } } - -// convertProviderType is a helper function for NewResource and NewDataSource -// implementations to associate the concrete provider type. Alternatively, -// this helper can be skipped and the provider type can be directly type -// asserted (e.g. provider: in.(*provider)), however using this can prevent -// potential panics. -func convertProviderType(in tfsdkprovider.Provider) (provider, diag.Diagnostics) { - var diags diag.Diagnostics - - p, ok := in.(*provider) - - if !ok { - diags.AddError( - "Unexpected Provider Instance Type", - fmt.Sprintf("While creating the data source or resource, an unexpected provider type (%T) was received. This is always a bug in the provider code and should be reported to the provider developers.", p), - ) - return provider{}, diags - } - - if p == nil { - diags.AddError( - "Unexpected Provider Instance Type", - "While creating the data source or resource, an unexpected empty provider instance was received. This is always a bug in the provider code and should be reported to the provider developers.", - ) - return provider{}, diags - } - - return *p, diags -} diff --git a/internal/provider/volume_data_source.go b/internal/provider/volume_data_source.go index 92d2a4d..b5dac46 100644 --- a/internal/provider/volume_data_source.go +++ b/internal/provider/volume_data_source.go @@ -5,16 +5,11 @@ import ( "github.com/fly-apps/terraform-provider-fly/graphql" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) -// Ensure provider defined types fully satisfy framework interfaces -var _ tfsdkprovider.DataSourceType = volumeDataSourceType{} -var _ datasource.DataSource = volumeDataSource{} - -type volumeDataSourceType struct{} +var _ datasource.DataSourceWithConfigure = &volumeDataSource{} // Matches getSchema type volumeDataSourceOutput struct { @@ -26,7 +21,11 @@ type volumeDataSourceOutput struct { Internalid types.String `tfsdk:"internalid"` } -func (v volumeDataSourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (v volumeDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "fly_volume" +} + +func (v volumeDataSource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly volume resource", Attributes: map[string]tfsdk.Attribute{ @@ -65,12 +64,8 @@ func (v volumeDataSourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Dia }, nil } -func (v volumeDataSourceType) NewDataSource(_ context.Context, in tfsdkprovider.Provider) (datasource.DataSource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return volumeDataSource{ - provider: provider, - }, diags +func NewVolumeDataSource() datasource.DataSource { + return volumeDataSource{} } func (v volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { @@ -82,7 +77,7 @@ func (v volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, internalId := data.Internalid.Value app := data.Appid.Value - query, err := graphql.VolumeQuery(context.Background(), *v.provider.client, app, internalId) + query, err := graphql.VolumeQuery(context.Background(), v.gqlClient, app, internalId) if err != nil { resp.Diagnostics.AddError("Read: query failed", err.Error()) } diff --git a/internal/provider/volume_resource.go b/internal/provider/volume_resource.go index 4329495..7ea4714 100644 --- a/internal/provider/volume_resource.go +++ b/internal/provider/volume_resource.go @@ -6,21 +6,21 @@ import ( "github.com/fly-apps/terraform-provider-fly/graphql" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - tfsdkprovider "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" ) -var _ tfsdkprovider.ResourceType = flyVolumeResourceType{} -var _ resource.Resource = flyVolumeResource{} -var _ resource.ResourceWithImportState = flyVolumeResource{} - -type flyVolumeResourceType struct{} +var _ resource.ResourceWithConfigure = &flyVolumeResource{} +var _ resource.ResourceWithImportState = &flyVolumeResource{} type flyVolumeResource struct { - provider provider + flyResource +} + +func newFlyVolumeResource() resource.Resource { + return &flyVolumeResource{} } type flyVolumeResourceData struct { @@ -32,7 +32,11 @@ type flyVolumeResourceData struct { Internalid types.String `tfsdk:"internalid"` } -func (t flyVolumeResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (vr flyVolumeResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "fly_volume" +} + +func (vr flyVolumeResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ MarkdownDescription: "Fly volume resource", Attributes: map[string]tfsdk.Attribute{ @@ -72,21 +76,13 @@ func (t flyVolumeResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Di }, nil } -func (t flyVolumeResourceType) NewResource(ctx context.Context, in tfsdkprovider.Provider) (resource.Resource, diag.Diagnostics) { - provider, diags := convertProviderType(in) - - return flyVolumeResource{ - provider: provider, - }, diags -} - func (vr flyVolumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data flyVolumeResourceData diags := req.Plan.Get(ctx, &data) resp.Diagnostics.Append(diags...) - q, err := graphql.CreateVolume(context.Background(), *vr.provider.client, data.Appid.Value, data.Name.Value, data.Region.Value, int(data.Size.Value)) + q, err := graphql.CreateVolume(context.Background(), vr.gqlClient, data.Appid.Value, data.Name.Value, data.Region.Value, int(data.Size.Value)) if err != nil { resp.Diagnostics.AddError("Failed to create volume", err.Error()) } @@ -118,7 +114,7 @@ func (vr flyVolumeResource) Read(ctx context.Context, req resource.ReadRequest, internalId := data.Internalid.Value app := data.Appid.Value - query, err := graphql.VolumeQuery(context.Background(), *vr.provider.client, app, internalId) + query, err := graphql.VolumeQuery(context.Background(), vr.gqlClient, app, internalId) if err != nil { resp.Diagnostics.AddError("Read: query failed", err.Error()) } @@ -151,7 +147,7 @@ func (vr flyVolumeResource) Delete(ctx context.Context, req resource.DeleteReque resp.Diagnostics.Append(diags...) if !data.Id.Unknown && !data.Id.Null && data.Id.Value != "" { - _, err := graphql.DeleteVolume(context.Background(), *vr.provider.client, data.Id.Value) + _, err := graphql.DeleteVolume(context.Background(), vr.gqlClient, data.Id.Value) if err != nil { resp.Diagnostics.AddError("Delete volume failed", err.Error()) } From 7a8c98ab55930813353ce25a4d478f5f21077a2d Mon Sep 17 00:00:00 2001 From: Lukas W Date: Mon, 31 Oct 2022 10:15:48 +0100 Subject: [PATCH 2/7] Refactor App resource GQL Use a fragment to create a common type for app query & mutation, reducing duplicate code --- graphql/generated.go | 164 +++++++++++++++++++++--------- graphql/genqlient.graphql | 32 ++++-- internal/provider/app_resource.go | 28 +++-- 3 files changed, 149 insertions(+), 75 deletions(-) diff --git a/graphql/generated.go b/graphql/generated.go index 3f3a3ae..ca0ce5d 100644 --- a/graphql/generated.go +++ b/graphql/generated.go @@ -183,6 +183,42 @@ func (v *AllocateIpAddressResponse) GetAllocateIpAddress() AllocateIpAddressAllo return v.AllocateIpAddress } +// AppFragment includes the GraphQL fields of App requested by the fragment AppFragment. +type AppFragment struct { + Id string `json:"id"` + Name string `json:"name"` + Organization AppFragmentOrganization `json:"organization"` + AppUrl string `json:"appUrl"` + PlatformVersion PlatformVersionEnum `json:"platformVersion"` +} + +// GetId returns AppFragment.Id, and is useful for accessing the field via an interface. +func (v *AppFragment) GetId() string { return v.Id } + +// GetName returns AppFragment.Name, and is useful for accessing the field via an interface. +func (v *AppFragment) GetName() string { return v.Name } + +// GetOrganization returns AppFragment.Organization, and is useful for accessing the field via an interface. +func (v *AppFragment) GetOrganization() AppFragmentOrganization { return v.Organization } + +// GetAppUrl returns AppFragment.AppUrl, and is useful for accessing the field via an interface. +func (v *AppFragment) GetAppUrl() string { return v.AppUrl } + +// GetPlatformVersion returns AppFragment.PlatformVersion, and is useful for accessing the field via an interface. +func (v *AppFragment) GetPlatformVersion() PlatformVersionEnum { return v.PlatformVersion } + +// AppFragmentOrganization includes the requested fields of the GraphQL type Organization. +type AppFragmentOrganization struct { + Id string `json:"id"` + Slug string `json:"slug"` +} + +// GetId returns AppFragmentOrganization.Id, and is useful for accessing the field via an interface. +func (v *AppFragmentOrganization) GetId() string { return v.Id } + +// GetSlug returns AppFragmentOrganization.Slug, and is useful for accessing the field via an interface. +func (v *AppFragmentOrganization) GetSlug() string { return v.Slug } + type AutoscaleRegionConfigInput struct { Code string `json:"code"` Weight int `json:"weight"` @@ -204,51 +240,11 @@ func (v *AutoscaleRegionConfigInput) GetReset() bool { return v.Reset } // CreateAppMutationCreateAppCreateAppPayload includes the requested fields of the GraphQL type CreateAppPayload. type CreateAppMutationCreateAppCreateAppPayload struct { - App CreateAppMutationCreateAppCreateAppPayloadApp `json:"app"` + App AppFragment `json:"app"` } // GetApp returns CreateAppMutationCreateAppCreateAppPayload.App, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayload) GetApp() CreateAppMutationCreateAppCreateAppPayloadApp { - return v.App -} - -// CreateAppMutationCreateAppCreateAppPayloadApp includes the requested fields of the GraphQL type App. -type CreateAppMutationCreateAppCreateAppPayloadApp struct { - Id string `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Organization CreateAppMutationCreateAppCreateAppPayloadAppOrganization `json:"organization"` - AppUrl string `json:"appUrl"` -} - -// GetId returns CreateAppMutationCreateAppCreateAppPayloadApp.Id, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadApp) GetId() string { return v.Id } - -// GetName returns CreateAppMutationCreateAppCreateAppPayloadApp.Name, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadApp) GetName() string { return v.Name } - -// GetStatus returns CreateAppMutationCreateAppCreateAppPayloadApp.Status, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadApp) GetStatus() string { return v.Status } - -// GetOrganization returns CreateAppMutationCreateAppCreateAppPayloadApp.Organization, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadApp) GetOrganization() CreateAppMutationCreateAppCreateAppPayloadAppOrganization { - return v.Organization -} - -// GetAppUrl returns CreateAppMutationCreateAppCreateAppPayloadApp.AppUrl, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadApp) GetAppUrl() string { return v.AppUrl } - -// CreateAppMutationCreateAppCreateAppPayloadAppOrganization includes the requested fields of the GraphQL type Organization. -type CreateAppMutationCreateAppCreateAppPayloadAppOrganization struct { - Id string `json:"id"` - Slug string `json:"slug"` -} - -// GetId returns CreateAppMutationCreateAppCreateAppPayloadAppOrganization.Id, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadAppOrganization) GetId() string { return v.Id } - -// GetSlug returns CreateAppMutationCreateAppCreateAppPayloadAppOrganization.Slug, and is useful for accessing the field via an interface. -func (v *CreateAppMutationCreateAppCreateAppPayloadAppOrganization) GetSlug() string { return v.Slug } +func (v *CreateAppMutationCreateAppCreateAppPayload) GetApp() AppFragment { return v.App } // CreateAppMutationResponse is returned by CreateAppMutation on success. type CreateAppMutationResponse struct { @@ -448,6 +444,14 @@ func (v *DeleteVolumeResponse) GetDeleteVolume() DeleteVolumeDeleteVolumeDeleteV return v.DeleteVolume } +// GetAppResponse is returned by GetApp on success. +type GetAppResponse struct { + App AppFragment `json:"app"` +} + +// GetApp returns GetAppResponse.App, and is useful for accessing the field via an interface. +func (v *GetAppResponse) GetApp() AppFragment { return v.App } + // GetCertificateApp includes the requested fields of the GraphQL type App. type GetCertificateApp struct { Certificate GetCertificateAppCertificate `json:"certificate"` @@ -1017,6 +1021,13 @@ func (v *OrgsQueryResponse) GetOrganizations() OrgsQueryOrganizationsOrganizatio return v.Organizations } +type PlatformVersionEnum string + +const ( + PlatformVersionEnumNomad PlatformVersionEnum = "nomad" + PlatformVersionEnumMachines PlatformVersionEnum = "machines" +) + // ReleaseIpAddressReleaseIpAddressReleaseIPAddressPayload includes the requested fields of the GraphQL type ReleaseIPAddressPayload. type ReleaseIpAddressReleaseIpAddressReleaseIPAddressPayload struct { App ReleaseIpAddressReleaseIpAddressReleaseIPAddressPayloadApp `json:"app"` @@ -1380,6 +1391,14 @@ type __DeleteVolumeInput struct { // GetVolume returns __DeleteVolumeInput.Volume, and is useful for accessing the field via an interface. func (v *__DeleteVolumeInput) GetVolume() string { return v.Volume } +// __GetAppInput is used internally by genqlient +type __GetAppInput struct { + Name string `json:"name"` +} + +// GetName returns __GetAppInput.Name, and is useful for accessing the field via an interface. +func (v *__GetAppInput) GetName() string { return v.Name } + // __GetCertificateInput is used internally by genqlient type __GetCertificateInput struct { App string `json:"app"` @@ -1603,17 +1622,20 @@ func CreateAppMutation( mutation CreateAppMutation ($name: String, $organizationId: ID!) { createApp(input: {name:$name,organizationId:$organizationId}) { app { - id - name - status - organization { - id - slug - } - appUrl + ... AppFragment } } } +fragment AppFragment on App { + id + name + organization { + id + slug + } + appUrl + platformVersion +} `, Variables: &__CreateAppMutationInput{ Name: name, @@ -1835,6 +1857,48 @@ mutation DeleteVolume ($volume: ID!) { return &data, err } +func GetApp( + ctx context.Context, + client graphql.Client, + name string, +) (*GetAppResponse, error) { + req := &graphql.Request{ + OpName: "GetApp", + Query: ` +query GetApp ($name: String) { + app(name: $name) { + ... AppFragment + } +} +fragment AppFragment on App { + id + name + organization { + id + slug + } + appUrl + platformVersion +} +`, + Variables: &__GetAppInput{ + Name: name, + }, + } + var err error + + var data GetAppResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func GetCertificate( ctx context.Context, client graphql.Client, diff --git a/graphql/genqlient.graphql b/graphql/genqlient.graphql index 2c68866..11cfb6e 100644 --- a/graphql/genqlient.graphql +++ b/graphql/genqlient.graphql @@ -41,17 +41,29 @@ query GetFullApp($name: String) { } } +fragment AppFragment on App { + id + name + organization { + id + slug + } + appUrl + platformVersion +} + +query GetApp($name: String) { + # @genqlient(flatten: true) + app(name: $name) { + ...AppFragment + } +} + mutation CreateAppMutation($name: String, $organizationId: ID!) { createApp(input: {name: $name, organizationId: $organizationId}) { + # @genqlient(flatten: true) app { - id - name - status - organization { - id - slug - } - appUrl + ...AppFragment } } } @@ -225,7 +237,7 @@ mutation CreatePostgresCluster( } mutation AddWireguardPeer( - $input: AddWireGuardPeerInput! + $input: AddWireGuardPeerInput! ) { addWireGuardPeer(input: $input) { network @@ -257,4 +269,4 @@ query Organization($slug: String) { organization(slug: $slug) { id } -} \ No newline at end of file +} diff --git a/internal/provider/app_resource.go b/internal/provider/app_resource.go index e305f68..0974da7 100644 --- a/internal/provider/app_resource.go +++ b/internal/provider/app_resource.go @@ -27,6 +27,16 @@ type flyAppResourceData struct { //Secrets types.Map `tfsdk:"secrets"` } +func appDataFromGraphql(f graphql.AppFragment) flyAppResourceData { + return flyAppResourceData{ + Name: types.String{Value: f.Name}, + Org: types.String{Value: f.Organization.Slug}, + OrgId: types.String{Value: f.Organization.Id}, + AppUrl: types.String{Value: f.AppUrl}, + Id: types.String{Value: f.Id}, + } +} + func (r flyAppResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "fly_app" } @@ -113,13 +123,7 @@ func (r flyAppResource) Create(ctx context.Context, req resource.CreateRequest, return } - data = flyAppResourceData{ - Org: types.String{Value: mresp.CreateApp.App.Organization.Slug}, - OrgId: types.String{Value: mresp.CreateApp.App.Organization.Id}, - Name: types.String{Value: mresp.CreateApp.App.Name}, - AppUrl: types.String{Value: mresp.CreateApp.App.AppUrl}, - Id: types.String{Value: mresp.CreateApp.App.Id}, - } + data = appDataFromGraphql(mresp.CreateApp.App) //if len(data.Secrets.Elems) > 0 { // var rawSecrets map[string]string @@ -160,7 +164,7 @@ func (r flyAppResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - query, err := graphql.GetFullApp(context.Background(), r.gqlClient, state.Name.Value) + query, err := graphql.GetApp(context.Background(), r.gqlClient, state.Name.Value) var errList gqlerror.List if errors.As(err, &errList) { for _, err := range errList { @@ -173,13 +177,7 @@ func (r flyAppResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.Diagnostics.AddError("Read: query failed", err.Error()) } - data := flyAppResourceData{ - Name: types.String{Value: query.App.Name}, - Org: types.String{Value: query.App.Organization.Slug}, - OrgId: types.String{Value: query.App.Organization.Id}, - AppUrl: types.String{Value: query.App.AppUrl}, - Id: types.String{Value: query.App.Id}, - } + data := appDataFromGraphql(query.App) //if !state.Secrets.Null && !state.Secrets.Unknown { // data.Secrets = state.Secrets From 937d0b7be12c4f25de6b20e6ff9fe7faa5587df9 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Fri, 4 Nov 2022 10:25:57 +0100 Subject: [PATCH 3/7] Allow specifying secrets in app resource --- graphql/generated.go | 235 +++++++++++++++- graphql/genqlient.graphql | 27 ++ graphql/genqlient.yaml | 2 + internal/provider/app_resource.go | 293 +++++++++++++++----- internal/provider/modifiers/use_state_if.go | 62 +++++ 5 files changed, 529 insertions(+), 90 deletions(-) create mode 100644 internal/provider/modifiers/use_state_if.go diff --git a/graphql/generated.go b/graphql/generated.go index ca0ce5d..d712438 100644 --- a/graphql/generated.go +++ b/graphql/generated.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/Khan/genqlient/graphql" ) @@ -185,11 +186,12 @@ func (v *AllocateIpAddressResponse) GetAllocateIpAddress() AllocateIpAddressAllo // AppFragment includes the GraphQL fields of App requested by the fragment AppFragment. type AppFragment struct { - Id string `json:"id"` - Name string `json:"name"` - Organization AppFragmentOrganization `json:"organization"` - AppUrl string `json:"appUrl"` - PlatformVersion PlatformVersionEnum `json:"platformVersion"` + Id string `json:"id"` + Name string `json:"name"` + Organization AppFragmentOrganization `json:"organization"` + Secrets []AppFragmentSecretsSecret `json:"secrets"` + AppUrl string `json:"appUrl"` + PlatformVersion PlatformVersionEnum `json:"platformVersion"` } // GetId returns AppFragment.Id, and is useful for accessing the field via an interface. @@ -201,6 +203,9 @@ func (v *AppFragment) GetName() string { return v.Name } // GetOrganization returns AppFragment.Organization, and is useful for accessing the field via an interface. func (v *AppFragment) GetOrganization() AppFragmentOrganization { return v.Organization } +// GetSecrets returns AppFragment.Secrets, and is useful for accessing the field via an interface. +func (v *AppFragment) GetSecrets() []AppFragmentSecretsSecret { return v.Secrets } + // GetAppUrl returns AppFragment.AppUrl, and is useful for accessing the field via an interface. func (v *AppFragment) GetAppUrl() string { return v.AppUrl } @@ -219,6 +224,22 @@ func (v *AppFragmentOrganization) GetId() string { return v.Id } // GetSlug returns AppFragmentOrganization.Slug, and is useful for accessing the field via an interface. func (v *AppFragmentOrganization) GetSlug() string { return v.Slug } +// AppFragmentSecretsSecret includes the requested fields of the GraphQL type Secret. +type AppFragmentSecretsSecret struct { + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + Digest string `json:"digest"` +} + +// GetName returns AppFragmentSecretsSecret.Name, and is useful for accessing the field via an interface. +func (v *AppFragmentSecretsSecret) GetName() string { return v.Name } + +// GetCreatedAt returns AppFragmentSecretsSecret.CreatedAt, and is useful for accessing the field via an interface. +func (v *AppFragmentSecretsSecret) GetCreatedAt() time.Time { return v.CreatedAt } + +// GetDigest returns AppFragmentSecretsSecret.Digest, and is useful for accessing the field via an interface. +func (v *AppFragmentSecretsSecret) GetDigest() string { return v.Digest } + type AutoscaleRegionConfigInput struct { Code string `json:"code"` Weight int `json:"weight"` @@ -927,6 +948,38 @@ type GetFullAppResponse struct { // GetApp returns GetFullAppResponse.App, and is useful for accessing the field via an interface. func (v *GetFullAppResponse) GetApp() GetFullAppApp { return v.App } +// GetSecretsApp includes the requested fields of the GraphQL type App. +type GetSecretsApp struct { + Secrets []GetSecretsAppSecretsSecret `json:"secrets"` +} + +// GetSecrets returns GetSecretsApp.Secrets, and is useful for accessing the field via an interface. +func (v *GetSecretsApp) GetSecrets() []GetSecretsAppSecretsSecret { return v.Secrets } + +// GetSecretsAppSecretsSecret includes the requested fields of the GraphQL type Secret. +type GetSecretsAppSecretsSecret struct { + Name string `json:"name"` + Digest string `json:"digest"` + CreatedAt time.Time `json:"createdAt"` +} + +// GetName returns GetSecretsAppSecretsSecret.Name, and is useful for accessing the field via an interface. +func (v *GetSecretsAppSecretsSecret) GetName() string { return v.Name } + +// GetDigest returns GetSecretsAppSecretsSecret.Digest, and is useful for accessing the field via an interface. +func (v *GetSecretsAppSecretsSecret) GetDigest() string { return v.Digest } + +// GetCreatedAt returns GetSecretsAppSecretsSecret.CreatedAt, and is useful for accessing the field via an interface. +func (v *GetSecretsAppSecretsSecret) GetCreatedAt() time.Time { return v.CreatedAt } + +// GetSecretsResponse is returned by GetSecrets on success. +type GetSecretsResponse struct { + App GetSecretsApp `json:"app"` +} + +// GetApp returns GetSecretsResponse.App, and is useful for accessing the field via an interface. +func (v *GetSecretsResponse) GetApp() GetSecretsApp { return v.App } + type IPAddressType string const ( @@ -1147,21 +1200,69 @@ func (v *SetSecretsResponse) GetSetSecrets() SetSecretsSetSecretsSetSecretsPaylo // SetSecretsSetSecretsSetSecretsPayload includes the requested fields of the GraphQL type SetSecretsPayload. type SetSecretsSetSecretsSetSecretsPayload struct { - Release SetSecretsSetSecretsSetSecretsPayloadRelease `json:"release"` + App SetSecretsSetSecretsSetSecretsPayloadApp `json:"app"` } -// GetRelease returns SetSecretsSetSecretsSetSecretsPayload.Release, and is useful for accessing the field via an interface. -func (v *SetSecretsSetSecretsSetSecretsPayload) GetRelease() SetSecretsSetSecretsSetSecretsPayloadRelease { +// GetApp returns SetSecretsSetSecretsSetSecretsPayload.App, and is useful for accessing the field via an interface. +func (v *SetSecretsSetSecretsSetSecretsPayload) GetApp() SetSecretsSetSecretsSetSecretsPayloadApp { + return v.App +} + +// SetSecretsSetSecretsSetSecretsPayloadApp includes the requested fields of the GraphQL type App. +type SetSecretsSetSecretsSetSecretsPayloadApp struct { + Secrets []SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret `json:"secrets"` +} + +// GetSecrets returns SetSecretsSetSecretsSetSecretsPayloadApp.Secrets, and is useful for accessing the field via an interface. +func (v *SetSecretsSetSecretsSetSecretsPayloadApp) GetSecrets() []SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret { + return v.Secrets +} + +// SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret includes the requested fields of the GraphQL type Secret. +type SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret struct { + Name string `json:"name"` + Digest string `json:"digest"` + CreatedAt time.Time `json:"createdAt"` +} + +// GetName returns SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret.Name, and is useful for accessing the field via an interface. +func (v *SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret) GetName() string { return v.Name } + +// GetDigest returns SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret.Digest, and is useful for accessing the field via an interface. +func (v *SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret) GetDigest() string { return v.Digest } + +// GetCreatedAt returns SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret.CreatedAt, and is useful for accessing the field via an interface. +func (v *SetSecretsSetSecretsSetSecretsPayloadAppSecretsSecret) GetCreatedAt() time.Time { + return v.CreatedAt +} + +// UnsetSecretsResponse is returned by UnsetSecrets on success. +type UnsetSecretsResponse struct { + UnsetSecrets UnsetSecretsUnsetSecretsUnsetSecretsPayload `json:"unsetSecrets"` +} + +// GetUnsetSecrets returns UnsetSecretsResponse.UnsetSecrets, and is useful for accessing the field via an interface. +func (v *UnsetSecretsResponse) GetUnsetSecrets() UnsetSecretsUnsetSecretsUnsetSecretsPayload { + return v.UnsetSecrets +} + +// UnsetSecretsUnsetSecretsUnsetSecretsPayload includes the requested fields of the GraphQL type UnsetSecretsPayload. +type UnsetSecretsUnsetSecretsUnsetSecretsPayload struct { + Release UnsetSecretsUnsetSecretsUnsetSecretsPayloadRelease `json:"release"` +} + +// GetRelease returns UnsetSecretsUnsetSecretsUnsetSecretsPayload.Release, and is useful for accessing the field via an interface. +func (v *UnsetSecretsUnsetSecretsUnsetSecretsPayload) GetRelease() UnsetSecretsUnsetSecretsUnsetSecretsPayloadRelease { return v.Release } -// SetSecretsSetSecretsSetSecretsPayloadRelease includes the requested fields of the GraphQL type Release. -type SetSecretsSetSecretsSetSecretsPayloadRelease struct { +// UnsetSecretsUnsetSecretsUnsetSecretsPayloadRelease includes the requested fields of the GraphQL type Release. +type UnsetSecretsUnsetSecretsUnsetSecretsPayloadRelease struct { Id string `json:"id"` } -// GetId returns SetSecretsSetSecretsSetSecretsPayloadRelease.Id, and is useful for accessing the field via an interface. -func (v *SetSecretsSetSecretsSetSecretsPayloadRelease) GetId() string { return v.Id } +// GetId returns UnsetSecretsUnsetSecretsUnsetSecretsPayloadRelease.Id, and is useful for accessing the field via an interface. +func (v *UnsetSecretsUnsetSecretsUnsetSecretsPayloadRelease) GetId() string { return v.Id } // UpdateAutoScaleConfigMutationResponse is returned by UpdateAutoScaleConfigMutation on success. type UpdateAutoScaleConfigMutationResponse struct { @@ -1419,6 +1520,14 @@ type __GetFullAppInput struct { // GetName returns __GetFullAppInput.Name, and is useful for accessing the field via an interface. func (v *__GetFullAppInput) GetName() string { return v.Name } +// __GetSecretsInput is used internally by genqlient +type __GetSecretsInput struct { + Name string `json:"name"` +} + +// GetName returns __GetSecretsInput.Name, and is useful for accessing the field via an interface. +func (v *__GetSecretsInput) GetName() string { return v.Name } + // __IpAddressQueryInput is used internally by genqlient type __IpAddressQueryInput struct { App string `json:"app"` @@ -1463,6 +1572,18 @@ type __SetSecretsInput struct { // GetInput returns __SetSecretsInput.Input, and is useful for accessing the field via an interface. func (v *__SetSecretsInput) GetInput() SetSecretsInput { return v.Input } +// __UnsetSecretsInput is used internally by genqlient +type __UnsetSecretsInput struct { + AppId string `json:"appId"` + Keys []string `json:"keys"` +} + +// GetAppId returns __UnsetSecretsInput.AppId, and is useful for accessing the field via an interface. +func (v *__UnsetSecretsInput) GetAppId() string { return v.AppId } + +// GetKeys returns __UnsetSecretsInput.Keys, and is useful for accessing the field via an interface. +func (v *__UnsetSecretsInput) GetKeys() []string { return v.Keys } + // __UpdateAutoScaleConfigMutationInput is used internally by genqlient type __UpdateAutoScaleConfigMutationInput struct { Id string `json:"id"` @@ -1633,6 +1754,11 @@ fragment AppFragment on App { id slug } + secrets { + name + createdAt + digest + } appUrl platformVersion } @@ -1877,6 +2003,11 @@ fragment AppFragment on App { id slug } + secrets { + name + createdAt + digest + } appUrl platformVersion } @@ -2010,6 +2141,42 @@ query GetFullApp ($name: String) { return &data, err } +func GetSecrets( + ctx context.Context, + client graphql.Client, + name string, +) (*GetSecretsResponse, error) { + req := &graphql.Request{ + OpName: "GetSecrets", + Query: ` +query GetSecrets ($name: String!) { + app(name: $name) { + secrets { + name + digest + createdAt + } + } +} +`, + Variables: &__GetSecretsInput{ + Name: name, + }, + } + var err error + + var data GetSecretsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func IpAddressQuery( ctx context.Context, client graphql.Client, @@ -2190,8 +2357,12 @@ func SetSecrets( Query: ` mutation SetSecrets ($input: SetSecretsInput!) { setSecrets(input: $input) { - release { - id + app { + secrets { + name + digest + createdAt + } } } } @@ -2214,6 +2385,42 @@ mutation SetSecrets ($input: SetSecretsInput!) { return &data, err } +func UnsetSecrets( + ctx context.Context, + client graphql.Client, + appId string, + keys []string, +) (*UnsetSecretsResponse, error) { + req := &graphql.Request{ + OpName: "UnsetSecrets", + Query: ` +mutation UnsetSecrets ($appId: ID!, $keys: [String!]!) { + unsetSecrets(input: {appId:$appId,keys:$keys}) { + release { + id + } + } +} +`, + Variables: &__UnsetSecretsInput{ + AppId: appId, + Keys: keys, + }, + } + var err error + + var data UnsetSecretsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func UpdateAutoScaleConfigMutation( ctx context.Context, client graphql.Client, diff --git a/graphql/genqlient.graphql b/graphql/genqlient.graphql index 11cfb6e..0cf70a1 100644 --- a/graphql/genqlient.graphql +++ b/graphql/genqlient.graphql @@ -48,6 +48,11 @@ fragment AppFragment on App { id slug } + secrets { + name + createdAt + digest + } appUrl platformVersion } @@ -257,8 +262,30 @@ mutation RemoveWireguardPeer( } } +query GetSecrets($name: String!) { + app(name: $name) { + secrets { + name + digest + createdAt + } + } +} + mutation SetSecrets($input: SetSecretsInput!) { setSecrets(input: $input) { + app { + secrets { + name + digest + createdAt + } + } + } +} + +mutation UnsetSecrets($appId: ID!, $keys: [String!]!) { + unsetSecrets(input: {appId: $appId, keys: $keys}) { release { id } diff --git a/graphql/genqlient.yaml b/graphql/genqlient.yaml index 59be017..f3f9eb6 100644 --- a/graphql/genqlient.yaml +++ b/graphql/genqlient.yaml @@ -6,4 +6,6 @@ operations: bindings: JSON: type: interface{} + ISO8601DateTime: + type: time.Time generated: generated.go diff --git a/internal/provider/app_resource.go b/internal/provider/app_resource.go index 0974da7..e76a19b 100644 --- a/internal/provider/app_resource.go +++ b/internal/provider/app_resource.go @@ -5,7 +5,9 @@ import ( "errors" "fmt" "github.com/fly-apps/terraform-provider-fly/graphql" + "github.com/fly-apps/terraform-provider-fly/internal/provider/modifiers" "github.com/fly-apps/terraform-provider-fly/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -13,28 +15,88 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vektah/gqlparser/v2/gqlerror" + "strings" + "time" ) var _ resource.ResourceWithConfigure = &flyAppResource{} var _ resource.ResourceWithImportState = &flyAppResource{} +type appSecret struct { + Value types.String `tfsdk:"value"` + Digest types.String `tfsdk:"digest"` + CreatedAt types.String `tfsdk:"created_at"` +} + +var secretType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "value": types.StringType, + "digest": types.StringType, + "created_at": types.StringType, + }, +} + +type secretMap map[string]appSecret + +//var secretMapType = types.MapType{ElemType: secretType} + +//type secretMap types.Map + type flyAppResourceData struct { - Name types.String `tfsdk:"name"` - Org types.String `tfsdk:"org"` - OrgId types.String `tfsdk:"orgid"` - AppUrl types.String `tfsdk:"appurl"` - Id types.String `tfsdk:"id"` - //Secrets types.Map `tfsdk:"secrets"` + Name types.String `tfsdk:"name"` + Org types.String `tfsdk:"org"` + OrgId types.String `tfsdk:"orgid"` + AppUrl types.String `tfsdk:"appurl"` + Id types.String `tfsdk:"id"` + Secrets secretMap `tfsdk:"secrets"` +} + +func (d *flyAppResourceData) setSecret(name string, secret appSecret) { + if d.Secrets == nil { + d.Secrets = make(secretMap) + } + d.Secrets[name] = secret } -func appDataFromGraphql(f graphql.AppFragment) flyAppResourceData { - return flyAppResourceData{ - Name: types.String{Value: f.Name}, - Org: types.String{Value: f.Organization.Slug}, - OrgId: types.String{Value: f.Organization.Id}, - AppUrl: types.String{Value: f.AppUrl}, - Id: types.String{Value: f.Id}, +func (d *flyAppResourceData) secretValues() map[string]string { + values := make(map[string]string, len(d.Secrets)) + for k, v := range d.Secrets { + values[k] = v.Value.Value } + return values +} + +func (d *flyAppResourceData) updateFromApi(a graphql.AppFragment) { + d.Name = types.String{Value: a.Name} + d.Org = types.String{Value: a.Organization.Slug} + d.OrgId = types.String{Value: a.Organization.Id} + d.AppUrl = types.String{Value: a.AppUrl} + d.Id = types.String{Value: a.Id} +} + +func (d *flyAppResourceData) updateSecretsFromApi(a graphql.AppFragment) { + for k := range d.Secrets { + d.updateSecretFromApi(k, a) + } +} + +func (d *flyAppResourceData) updateSecretFromApi(name string, a graphql.AppFragment) { + s := d.Secrets[name] + for _, as := range a.Secrets { + if as.Name != name { + continue + } + if as.Digest != s.Digest.Value || as.CreatedAt.Format(time.RFC3339) != s.CreatedAt.Value { + d.Secrets[name] = appSecret{ + Digest: types.String{Value: as.Digest}, + CreatedAt: types.String{Value: as.CreatedAt.Format(time.RFC3339)}, + Value: types.String{Unknown: true}, + } + } + return + } + // Not found in app, so secret was removed outside of Terraform + delete(d.Secrets, name) } func (r flyAppResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -42,6 +104,14 @@ func (r flyAppResource) Metadata(_ context.Context, _ resource.MetadataRequest, } func (r flyAppResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + var secretValueUnchanged modifiers.UseStateForUnknownIfFunc = func(ctx context.Context, req tfsdk.ModifyAttributePlanRequest) (ok bool, diags diag.Diagnostics) { + valuePath := req.AttributePath.ParentPath().AtName("value") + var stateValue, configValue types.String + diags.Append(req.State.GetAttribute(ctx, valuePath, &stateValue)...) + diags.Append(req.Config.GetAttribute(ctx, valuePath, &configValue)...) + return stateValue.Equal(configValue), diags + } + return tfsdk.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Fly app resource", @@ -58,6 +128,26 @@ func (r flyAppResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnosti MarkdownDescription: "Optional org slug to operate upon", Type: types.StringType, }, + "secrets": { + Optional: true, + Attributes: tfsdk.MapNestedAttributes(map[string]tfsdk.Attribute{ + "value": { + Type: types.StringType, + Sensitive: true, + Required: true, + }, + "digest": { + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{modifiers.UseStateForUnknownIf(secretValueUnchanged)}, + }, + "created_at": { + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{modifiers.UseStateForUnknownIf(secretValueUnchanged)}, + }, + }), + }, "orgid": { Computed: true, MarkdownDescription: "readonly orgid", @@ -73,12 +163,6 @@ func (r flyAppResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnosti MarkdownDescription: "readonly appUrl", Type: types.StringType, }, - //"secrets": { - // Sensitive: true, - // Optional: true, - // MarkdownDescription: "App secrets", - // Type: types.MapType{ElemType: types.StringType}, - //}, }, }, nil } @@ -122,36 +206,21 @@ func (r flyAppResource) Create(ctx context.Context, req resource.CreateRequest, resp.Diagnostics.AddError("Create app failed", err.Error()) return } + data.updateFromApi(mresp.CreateApp.App) - data = appDataFromGraphql(mresp.CreateApp.App) - - //if len(data.Secrets.Elems) > 0 { - // var rawSecrets map[string]string - // data.Secrets.ElementsAs(context.Background(), &rawSecrets, false) - // var secrets []graphql.SecretInput - // for k, v := range rawSecrets { - // secrets = append(secrets, graphql.SecretInput{ - // Key: k, - // Value: v, - // }) - // } - // _, err := graphql.SetSecrets(context.Background(), *r.gqlClient, graphql.SetSecretsInput{ - // AppId: data.Id.Value, - // Secrets: secrets, - // ReplaceAll: true, - // }) - // if err != nil { - // resp.Diagnostics.AddError("Could not set rawSecrets", err.Error()) - // return - // } - // data.Secrets = utils.KVToTfMap(rawSecrets, types.StringType) - //} - - diags = resp.State.Set(ctx, &data) - resp.Diagnostics.Append(diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) if resp.Diagnostics.HasError() { return } + + if len(data.Secrets) > 0 { + r.setSecrets(ctx, data.Name.Value, data.Secrets, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r flyAppResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -177,13 +246,10 @@ func (r flyAppResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.Diagnostics.AddError("Read: query failed", err.Error()) } - data := appDataFromGraphql(query.App) + state.updateFromApi(query.App) + state.updateSecretsFromApi(query.App) - //if !state.Secrets.Null && !state.Secrets.Unknown { - // data.Secrets = state.Secrets - //} - - diags = resp.State.Set(ctx, &data) + diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -213,33 +279,44 @@ func (r flyAppResource) Update(ctx context.Context, req resource.UpdateRequest, resp.Diagnostics.AddError("Can't mutate Name of existing app", "Can't switch name "+state.Name.Value+" to "+plan.Name.Value) } - //if len(plan.Secrets.Elems) > 0 { - // var rawSecrets map[string]string - // plan.Secrets.ElementsAs(context.Background(), &rawSecrets, false) - // var secrets []graphql.SecretInput - // for k, v := range rawSecrets { - // secrets = append(secrets, graphql.SecretInput{ - // Key: k, - // Value: v, - // }) - // } - // _, err := graphql.SetSecrets(context.Background(), r.gqlClient, graphql.SetSecretsInput{ - // AppId: state.Id.Value, - // Secrets: secrets, - // ReplaceAll: true, - // }) - // if err != nil { - // resp.Diagnostics.AddError("Could not set rawSecrets", err.Error()) - // return - // } - // state.Secrets = utils.KVToTfMap(rawSecrets, types.StringType) - //} - - resp.State.Set(ctx, state) + // Unset secrets that were removed from config + var removedSecrets []string + for k := range state.Secrets { + if _, ok := plan.Secrets[k]; !ok { + removedSecrets = append(removedSecrets, k) + delete(state.Secrets, k) + } + } + if len(removedSecrets) > 0 { + _, err := graphql.UnsetSecrets(ctx, r.gqlClient, state.Name.Value, removedSecrets) + if err != nil { + resp.Diagnostics.AddError("UnsetSecrets failed", err.Error()) + } else { + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + } + if resp.Diagnostics.HasError() { + return + } + } - if resp.Diagnostics.HasError() { - return + // Set secrets that were changed or newly appeared in config + newSecrets := make(secretMap) + for k, ps := range plan.Secrets { + if s, ok := state.Secrets[k]; !ok || !s.Value.Equal(ps.Value) { + newSecrets[k] = ps + } + } + if len(newSecrets) > 0 { + r.setSecrets(ctx, state.Name.Value, newSecrets, &resp.Diagnostics) + for k, s := range newSecrets { + state.setSecret(k, s) + } + if resp.Diagnostics.HasError() { + return + } } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } func (r flyAppResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { @@ -268,3 +345,67 @@ func (r flyAppResource) Delete(ctx context.Context, req resource.DeleteRequest, func (r flyAppResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) } + +// setSecrets sets the app's secrets specified in the secret map `secrets` and +// writes back `digest` and `createdAt` attributes. +func (r flyAppResource) setSecrets(ctx context.Context, appName string, secrets secretMap, diags *diag.Diagnostics) { + inputs := make([]graphql.SecretInput, len(secrets)) + i := 0 + for k, v := range secrets { + inputs[i] = graphql.SecretInput{ + Key: k, + Value: v.Value.Value, + } + i++ + } + + resp, err := graphql.SetSecrets(ctx, r.gqlClient, graphql.SetSecretsInput{ + AppId: appName, + Secrets: inputs, + ReplaceAll: false, + }) + if err != nil { + var errList gqlerror.List + if errors.As(err, &errList) && len(errList) == 1 && strings.Contains(errList[0].Message, "No change detected") { + diags.AddWarning("SetSecrets was no-op", err.Error()) + // Secrets may have been added outside of Terraform. To prevent unknown value errors, we need to fetch + // digest and createdAt attributes for secrets that don't have those values missing in state + diags.AddWarning("State may have drifted", "Secrets may have been added or changed outside of Terraform. Refreshing secret state, you may need to re-apply your Terraform config.") + r.getSecretComputedValues(ctx, appName, secrets, diags) + } else { + diags.AddError("SetSecrets errored", err.Error()) + } + return + } + + for _, s := range resp.SetSecrets.App.Secrets { + secrets[s.Name] = appSecret{ + Value: secrets[s.Name].Value, + Digest: types.String{Value: s.Digest}, + CreatedAt: types.String{Value: s.CreatedAt.Format(time.RFC3339)}, + } + } +} + +func (r flyAppResource) getSecretComputedValues(ctx context.Context, appName string, secrets secretMap, diags *diag.Diagnostics) { + apiSecrets, err := graphql.GetSecrets(ctx, r.gqlClient, appName) + if err != nil { + diags.AddError("GetSecrets failed", err.Error()) + } + for _, as := range apiSecrets.App.Secrets { + if s, ok := secrets[as.Name]; ok { + secrets[as.Name] = appSecret{ + Value: s.Value, + Digest: types.String{Value: as.Digest}, + CreatedAt: types.String{Value: as.CreatedAt.Format(time.RFC3339)}, + } + } + } +} + +func (r flyAppResource) unsetSecrets(ctx context.Context, appName string, secretKeys []string, diags *diag.Diagnostics) { + _, err := graphql.UnsetSecrets(ctx, r.gqlClient, appName, secretKeys) + if err != nil { + diags.AddError("UnsetSecrets failed", err.Error()) + } +} diff --git a/internal/provider/modifiers/use_state_if.go b/internal/provider/modifiers/use_state_if.go new file mode 100644 index 0000000..ebf4e10 --- /dev/null +++ b/internal/provider/modifiers/use_state_if.go @@ -0,0 +1,62 @@ +package modifiers + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +type UseStateForUnknownIfFunc func(ctx context.Context, req tfsdk.ModifyAttributePlanRequest) (bool, diag.Diagnostics) + +// UseStateForUnknownIf is like UseStateForUnknown, but conditional +func UseStateForUnknownIf(condition UseStateForUnknownIfFunc) tfsdk.AttributePlanModifier { + return useStateForUnknownIfModifier{condition} +} + +// useStateForUnknownIfModifier implements the UseStateForUnknownIf +// AttributePlanModifier. +type useStateForUnknownIfModifier struct { + condition UseStateForUnknownIfFunc +} + +// Modify copies the attribute's prior state to the attribute plan if the prior +// state value is not null. +func (r useStateForUnknownIfModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + if req.AttributeState == nil || resp.AttributePlan == nil || req.AttributeConfig == nil { + return + } + + // if we have no state value, there's nothing to preserve + if req.AttributeState.IsNull() { + return + } + + // if it's not planned to be the unknown value, stick with the concrete plan + if !resp.AttributePlan.IsUnknown() { + return + } + + // if the config is the unknown value, use the unknown value otherwise, interpolation gets messed up + if req.AttributeConfig.IsUnknown() { + return + } + + ok, diags := r.condition(ctx, req) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + if !ok { + return + } + + resp.AttributePlan = req.AttributeState +} + +func (r useStateForUnknownIfModifier) Description(context.Context) string { + return "Once set, the value of this attribute in state will not change as long as the given condition holds." +} + +func (r useStateForUnknownIfModifier) MarkdownDescription(ctx context.Context) string { + return r.Description(ctx) +} From ed5ffb13e8c482683f288f05fe6e8aef4f83a451 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Wed, 2 Nov 2022 12:10:59 +0100 Subject: [PATCH 4/7] Tests: Remove duplicate provider config, allow setting org & region --- internal/provider/machine_resource_test.go | 80 +++++++--------------- internal/provider/provider_test.go | 29 ++++++++ 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/internal/provider/machine_resource_test.go b/internal/provider/machine_resource_test.go index 9037a2b..369df6c 100644 --- a/internal/provider/machine_resource_test.go +++ b/internal/provider/machine_resource_test.go @@ -31,15 +31,11 @@ func testFlyMachineResourceConfig(name string) string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" name = "%s" image = "nginx" env = { @@ -62,22 +58,18 @@ resource "fly_machine" "testMachine" { } ] } -`, app, name) +`, providerConfig(), app, getTestRegion(), name) } func testFlyMachineResourceUpdateConfig(name string) string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" name = "%s" image = "nginx" env = { @@ -100,7 +92,7 @@ resource "fly_machine" "testMachine" { } ] } -`, app, name) +`, providerConfig(), app, getTestRegion(), name) } func TestAccFlyMachineNoServices(t *testing.T) { @@ -122,22 +114,18 @@ func testFlyMachineResourceNoServicesConfig(name string) string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" name = "%s" image = "nginx" env = { updatedkey = "value" } } -`, app, name) +`, providerConfig(), app, getTestRegion(), name) } func TestAccFlyMachineEmptyServices(t *testing.T) { @@ -159,15 +147,11 @@ func testFlyMachineResourceEmptyServicesConfig(name string) string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" name = "%s" image = "nginx" env = { @@ -175,7 +159,7 @@ resource "fly_machine" "testMachine" { } services = [] } -`, app, name) +`, providerConfig(), app, getTestRegion(), name) } func TestAccFlyMachineInitOptions(t *testing.T) { @@ -202,15 +186,11 @@ func testFlyMachineResourceInitOptionsConfig(name string) string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" name = "%s" image = "nginx" env = { @@ -220,7 +200,7 @@ resource "fly_machine" "testMachine" { entrypoint = ["entrypointText"] exec = ["execText"] } -`, app, name) +`, providerConfig(), app, getTestRegion(), name) } func TestAccFlyMachineModifyImage(t *testing.T) { @@ -244,15 +224,11 @@ func testFlyMachineResourceChangeImageConfig(name string) string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" name = "%s" image = "nginx:latest" env = { @@ -275,7 +251,7 @@ resource "fly_machine" "testMachine" { } ] } -`, app, name) +`, providerConfig(), app, getTestRegion(), name) } func TestAccFlyMachineEmptyName(t *testing.T) { @@ -298,15 +274,11 @@ func testFlyMachineResourceEmptyNameConfig() string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" image = "nginx" env = { updatedkey = "value" @@ -328,22 +300,18 @@ resource "fly_machine" "testMachine" { } ] } -`, app) +`, providerConfig(), getTestRegion(), app) } func testFlyMachineResourceEmptyNameUpdateConfig() string { app := os.Getenv("FLY_TF_TEST_APP") return fmt.Sprintf(` -provider "fly" { - useinternaltunnel = true - internaltunnelorg = "fly-terraform-ci" - internaltunnelregion = "ewr" -} +%s resource "fly_machine" "testMachine" { app = "%s" - region = "ewr" + region = "%s" image = "nginx:latest" env = { updatedkey = "value" @@ -365,5 +333,5 @@ resource "fly_machine" "testMachine" { } ] } -`, app) +`, providerConfig(), getTestRegion(), app) } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index fb5e39b..a2572ec 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,6 +1,7 @@ package provider import ( + "fmt" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "os" @@ -21,3 +22,31 @@ func testAccPreCheck(t *testing.T) { t.Fatalf("Need app in FLY_TF_TEST_APP") } } + +func getTestOrg() string { + org, ok := os.LookupEnv("FLY_TF_TEST_ORG") + if ok { + return org + } else { + return "fly-terraform-ci" + } +} + +func getTestRegion() string { + region, ok := os.LookupEnv("FLY_TF_TEST_REGION") + if ok { + return region + } else { + return "ewr" + } +} + +func providerConfig() string { + return fmt.Sprintf(` +provider "fly" { + useinternaltunnel = true + internaltunnelorg = "%s" + internaltunnelregion = "%s" +} +`, getTestOrg(), getTestRegion()) +} From dc60c97ce32cc6072e1461fcc0debfa9a5e36b65 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Fri, 4 Nov 2022 11:26:15 +0100 Subject: [PATCH 5/7] Add app resource test --- internal/provider/app_resource.go | 2 +- internal/provider/app_resource_test.go | 222 +++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 internal/provider/app_resource_test.go diff --git a/internal/provider/app_resource.go b/internal/provider/app_resource.go index e76a19b..6b3b71b 100644 --- a/internal/provider/app_resource.go +++ b/internal/provider/app_resource.go @@ -369,7 +369,7 @@ func (r flyAppResource) setSecrets(ctx context.Context, appName string, secrets if errors.As(err, &errList) && len(errList) == 1 && strings.Contains(errList[0].Message, "No change detected") { diags.AddWarning("SetSecrets was no-op", err.Error()) // Secrets may have been added outside of Terraform. To prevent unknown value errors, we need to fetch - // digest and createdAt attributes for secrets that don't have those values missing in state + // digest and createdAt attributes for secrets that are missing those values in state diags.AddWarning("State may have drifted", "Secrets may have been added or changed outside of Terraform. Refreshing secret state, you may need to re-apply your Terraform config.") r.getSecretComputedValues(ctx, appName, secrets, diags) } else { diff --git a/internal/provider/app_resource_test.go b/internal/provider/app_resource_test.go new file mode 100644 index 0000000..d2d91e1 --- /dev/null +++ b/internal/provider/app_resource_test.go @@ -0,0 +1,222 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/Khan/genqlient/graphql" + providerGraphql "github.com/fly-apps/terraform-provider-fly/graphql" + "github.com/fly-apps/terraform-provider-fly/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "net/http" + "os" + "testing" + "time" +) + +func TestAccApp_basic(t *testing.T) { + appName := "testApp" + resourceName := fmt.Sprintf("fly_app.%s", appName) + name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + ctx := context.Background() + h := http.Client{Timeout: 60 * time.Second, Transport: &utils.Transport{UnderlyingTransport: http.DefaultTransport, Token: os.Getenv("FLY_API_TOKEN"), Ctx: context.Background()}} + client := graphql.NewClient("https://api.fly.io/graphql", &h) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +%s +resource "fly_app" "%s" { + name = "%s" + org = "%s" +} +`, providerConfig(), appName, name, getTestOrg()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "org", getTestOrg()), + resource.TestCheckResourceAttrSet(resourceName, "orgid"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrWith(resourceName, "id", func(id string) error { + app, err := providerGraphql.GetApp(ctx, client, id) + if err != nil { + t.Fatalf("Error in GetApp for %s: %v", id, err) + } + if app == nil { + t.Fatalf("GetApp for %s returned nil", id) + } + return nil + }), + ), + }, + }, + }) +} + +func TestAccApp_secrets(t *testing.T) { + appName := "testApp" + resourceName := fmt.Sprintf("fly_app.%s", appName) + name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + h := http.Client{Timeout: 60 * time.Second, Transport: &utils.Transport{UnderlyingTransport: http.DefaultTransport, Token: os.Getenv("FLY_API_TOKEN"), Ctx: context.Background()}} + client := graphql.NewClient("https://api.fly.io/graphql", &h) + + var lastDigest string + + testDigestEqualInApi := func(digest string) error { + r, err := providerGraphql.GetSecrets(context.Background(), client, name) + if err != nil { + t.Fatal(err) + } + apiDigest := r.App.Secrets[0].Digest + if digest != apiDigest { + return errors.New(fmt.Sprintf("Digest %s in resource differs from digest %s from API", digest, apiDigest)) + } + return nil + } + + testDigestChanged := func(digest string) error { + if digest == lastDigest { + return errors.New(fmt.Sprintf("digest %s did not change even though we changed the secret's value", digest)) + } + lastDigest = digest + return nil + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +%s +resource "fly_app" "%s" { + name = "%s" + org = "%s" + secrets = { + TEST = {value = "1"} + } +} +`, providerConfig(), appName, name, getTestOrg()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "secrets.TEST.value", "1"), + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.digest"), + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.created_at"), + resource.TestCheckResourceAttrWith(resourceName, "secrets.TEST.digest", testDigestEqualInApi), + resource.TestCheckResourceAttrWith(resourceName, "secrets.TEST.digest", func(digest string) error { + lastDigest = digest + return nil + }), + ), + }, + { + Config: fmt.Sprintf(` +%s +resource "fly_app" "%s" { + name = "%s" + org = "%s" + secrets = { + TEST = {value = "2"} + } +} +`, providerConfig(), appName, name, getTestOrg()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "secrets.TEST.value", "2"), + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.digest"), + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.created_at"), + resource.TestCheckResourceAttrWith(resourceName, "secrets.TEST.digest", testDigestChanged), + resource.TestCheckResourceAttrWith(resourceName, "secrets.TEST.digest", testDigestEqualInApi), + ), + }, + + // Secret drift detection: We change the secret's value using direct API calls outside + // and verify that the resource is able to detect this and restore the original state + { + PreConfig: func() { + r, err := providerGraphql.GetSecrets(context.Background(), client, name) + if err != nil { + t.Fatal(err) + } + oldSecret := r.App.Secrets[0] + sr, err := providerGraphql.SetSecrets(context.Background(), client, providerGraphql.SetSecretsInput{ + AppId: name, + Secrets: []providerGraphql.SecretInput{{ + Key: "TEST", + Value: "3", + }}, + }) + if err != nil { + t.Fatal(err) + } + r, err = providerGraphql.GetSecrets(context.Background(), client, name) + newSecret := r.App.Secrets[0] + if sr.SetSecrets.App.Secrets[0].Digest != newSecret.Digest { + t.Fatal("fly API SetSecrets returned different digest than subsequent GetSecrets") + } + if newSecret.Digest == oldSecret.Digest { + t.Fatal("fly API SetSecret had no effect") + } + }, + Config: fmt.Sprintf(` +%s +resource "fly_app" "%s" { + name = "%s" + org = "%s" + secrets = { + TEST = {value = "2"} + } +} +`, providerConfig(), appName, name, getTestOrg()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "secrets.TEST.value", "2"), + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.digest"), + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.created_at"), + resource.TestCheckResourceAttrWith(resourceName, "secrets.TEST.digest", testDigestEqualInApi), + func(state *terraform.State) error { + r, err := providerGraphql.GetSecrets(context.Background(), client, name) + if err != nil { + return err + } + if len(r.App.Secrets) != 1 { + return errors.New(fmt.Sprintf("Unexpected number of secrets %d", len(r.App.Secrets))) + } + if r.App.Secrets[0].Name != "TEST" { + return errors.New(fmt.Sprintf("Unexpected secret name %v", r.App.Secrets[0].Name)) + } + return nil + }, + resource.TestCheckResourceAttrWith(resourceName, "secrets.TEST.digest", func(value string) error { + r, err := providerGraphql.GetSecrets(context.Background(), client, name) + if err != nil { + return err + } + if r.App.Secrets[0].Digest != value { + return errors.New(fmt.Sprintf("digest in state (%s) differs from digest from fly API (%s)", value, r.App.Secrets[0].Digest)) + } + if value != lastDigest { + return errors.New(fmt.Sprintf("digest in state (%s) differs from digest of same value before drift (%s)", value, lastDigest)) + } + return nil + }), + ), + }, + + { + Config: fmt.Sprintf(` +%s +resource "fly_app" "%s" { + name = "%s" + org = "%s" + secrets = {} +} +`, providerConfig(), appName, name, getTestOrg()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckNoResourceAttr(resourceName, "secrets.TEST"), + ), + }, + }, + }) +} From f451e88f28c9b1996d67e112f7caec9a1a419f89 Mon Sep 17 00:00:00 2001 From: Lukas W Date: Wed, 9 Nov 2022 16:07:24 +0100 Subject: [PATCH 6/7] Fix app resource deleting unmanaged secrets in state --- internal/provider/app_resource.go | 6 +++- internal/provider/app_resource_test.go | 43 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/provider/app_resource.go b/internal/provider/app_resource.go index 6b3b71b..40ef7c3 100644 --- a/internal/provider/app_resource.go +++ b/internal/provider/app_resource.go @@ -129,7 +129,8 @@ func (r flyAppResource) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnosti Type: types.StringType, }, "secrets": { - Optional: true, + Optional: true, + Description: "Secret environment variables. Keys are case sensitive and are used as environment variable names. Does not override existing secrets added outside of Terraform.", Attributes: tfsdk.MapNestedAttributes(map[string]tfsdk.Attribute{ "value": { Type: types.StringType, @@ -379,6 +380,9 @@ func (r flyAppResource) setSecrets(ctx context.Context, appName string, secrets } for _, s := range resp.SetSecrets.App.Secrets { + if _, ok := secrets[s.Name]; !ok { + continue + } secrets[s.Name] = appSecret{ Value: secrets[s.Name].Value, Digest: types.String{Value: s.Digest}, diff --git a/internal/provider/app_resource_test.go b/internal/provider/app_resource_test.go index d2d91e1..6453f86 100644 --- a/internal/provider/app_resource_test.go +++ b/internal/provider/app_resource_test.go @@ -204,6 +204,49 @@ resource "fly_app" "%s" { ), }, + // Verify that we don't touch unmanaged secrets + { + PreConfig: func() { + _, err := providerGraphql.SetSecrets(context.Background(), client, providerGraphql.SetSecretsInput{ + AppId: name, + Secrets: []providerGraphql.SecretInput{{ + Key: "UNMANAGED", + Value: "1", + }}, + }) + if err != nil { + t.Fatal(err) + } + }, + Config: fmt.Sprintf(` +%s +resource "fly_app" "%s" { + name = "%s" + org = "%s" + secrets = { + TEST = {value = "3"} + } +} +`, providerConfig(), appName, name, getTestOrg()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "secrets.TEST.digest"), + resource.TestCheckNoResourceAttr(resourceName, "secrets.UNMANAGED.digest"), + func(state *terraform.State) error { + r, err := providerGraphql.GetSecrets(context.Background(), client, name) + if err != nil { + return err + } + + if len(r.App.Secrets) == 1 && r.App.Secrets[0].Name == "TEST" { + return errors.New("unmanaged secret disappeared") + } else if len(r.App.Secrets) != 2 { + return errors.New(fmt.Sprintf("unexpected secrets in API %v", r.App.Secrets)) + } + return nil + }, + ), + }, + { Config: fmt.Sprintf(` %s From e5fa37b140416967c554a549a555676fe8050044 Mon Sep 17 00:00:00 2001 From: lukas-w Date: Thu, 10 Nov 2022 06:50:26 +0000 Subject: [PATCH 7/7] Doc update --- docs/index.md | 2 +- docs/resources/app.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 25cbfc2..ce40fee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,7 +25,7 @@ provider "fly" { ### Optional - `fly_api_token` (String) fly.io api token. If not set checks env for FLY_API_TOKEN -- `fly_http_endpoint` (String) Where the provider should look to find the fly http endpoint +- `fly_http_endpoint` (String) Where the clients should look to find the fly http endpoint - `internaltunnelorg` (String) - `internaltunnelregion` (String) - `useinternaltunnel` (Boolean) diff --git a/docs/resources/app.md b/docs/resources/app.md index c532932..05104ab 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -28,6 +28,7 @@ resource "fly_app" "exampleApp" { ### Optional - `org` (String) Optional org slug to operate upon +- `secrets` (Attributes Map) Secret environment variables. Keys are case sensitive and are used as environment variable names. Does not override existing secrets added outside of Terraform. (see [below for nested schema](#nestedatt--secrets)) ### Read-Only @@ -35,4 +36,16 @@ resource "fly_app" "exampleApp" { - `id` (String) readonly app id - `orgid` (String) readonly orgid + +### Nested Schema for `secrets` + +Required: + +- `value` (String, Sensitive) + +Read-Only: + +- `created_at` (String) +- `digest` (String) +