diff --git a/README.md b/README.md index c9f14e8..245088e 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,29 @@ resource "render_service" "nextjs" { } } + +resource "render_service" "python_app" { + name = "my_python_app" + repo = "https://github.com/render-examples/flask-hello-world" + type = "web_service" + branch = "master" + + web_service_details = { + env = "python" + region = "frankfurt" + plan = "starter" + native = { + build_command = "pip install -r requirements.txt" + start_command = "gunicorn app:app" + } + disk = { + name = "my_app_data" + mount_path = "/my_app_data" + size_gb = 10 + } + } +} + resource "render_service" "mongodb" { name = "mongodb" repo = "https://github.com/render-examples/mongodb" diff --git a/examples/deploy-flask/main.tf b/examples/deploy-flask/main.tf index 9f12cf4..cc63d7a 100644 --- a/examples/deploy-flask/main.tf +++ b/examples/deploy-flask/main.tf @@ -19,7 +19,7 @@ resource "render_service" "flask" { branch = "master" web_service_details = { - env = "node" + env = "python" region = "frankfurt" plan = "starter" native = { diff --git a/render/models/service.go b/render/models/service.go index 2ff5701..8b926e9 100644 --- a/render/models/service.go +++ b/render/models/service.go @@ -13,11 +13,19 @@ type Service struct { Type types.String `tfsdk:"type"` Repo types.String `tfsdk:"repo"` Branch types.String `tfsdk:"branch"` + Image *ImageDetails `tfsdk:"image"` Owner types.String `tfsdk:"owner"` AutoDeploy types.Bool `tfsdk:"auto_deploy"` WebServiceDetails *WebServiceDetails `tfsdk:"web_service_details"` StaticSiteDetails *StaticSiteDetails `tfsdk:"static_site_details"` PrivateServiceDetails *PrivateServiceDetails `tfsdk:"private_service_details"` + EnvVars *[]EnvVar `tfsdk:"env_vars"` +} + +type EnvVar struct { + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + Generated types.Bool `tfsdk:"generated"` } type WebServiceDetails struct { @@ -28,6 +36,19 @@ type WebServiceDetails struct { HealthCheckPath types.String `tfsdk:"health_check_path"` Native *WebServiceDetailsNative `tfsdk:"native"` Url types.String `tfsdk:"url"` + Disk *Disk `tfsdk:"disk"` + Docker *WebServiceDetailsDocker `tfsdk:"docker"` +} + +type ImageDetails struct { + OwnerId types.String `tfsdk:"owner_id"` + ImagePath types.String `tfsdk:"image_path"` + RegistryCredentialId types.String `tfsdk:"registry_credential_id"` +} +type WebServiceDetailsDocker struct { + DockerCommand types.String `tfsdk:"command"` + DockerContext types.String `tfsdk:"context"` + DockerfilePath types.String `tfsdk:"path"` } type WebServiceDetailsNative struct { @@ -61,12 +82,22 @@ func (s Service) FromResponse(response render.Service) Service { serviceType := *response.Type service := Service{ - ID: fromStringOptional(response.Id), - Name: fromStringOptional(response.Name), - Type: fromServiceType(response.Type), - Repo: fromStringOptional(response.Repo), - Branch: fromStringOptional(response.Branch), - Owner: fromStringOptional(response.OwnerId), + ID: fromStringOptional(response.Id), + Name: fromStringOptional(response.Name), + Type: fromServiceType(response.Type), + Repo: fromStringOptional(response.Repo), + Branch: fromStringOptional(response.Branch), + Owner: fromStringOptional(response.OwnerId), + AutoDeploy: fromBoolOptional(response.AutoDeploy, true), //sub-optimal + EnvVars: s.EnvVars, + } + + if response.ImagePath != nil { + service.Image = &ImageDetails{ + OwnerId: fromStringOptional(response.OwnerId), + ImagePath: fromStringOptional(response.ImagePath), + RegistryCredentialId: fromStringOptional(response.RegistryCredentialId), + } } if serviceType == render.WebService { @@ -82,12 +113,37 @@ func (s Service) FromResponse(response render.Service) Service { native, err := details.EnvSpecificDetails.AsNativeEnvironmentDetails() - if err == nil { + if err == nil && s.WebServiceDetails.Native != nil { service.WebServiceDetails.Native = &WebServiceDetailsNative{ BuildCommand: fromStringOptional(native.BuildCommand), StartCommand: fromStringOptional(native.StartCommand), } } + if details.Disk != nil { + service.WebServiceDetails.Disk = &Disk{ + Name: fromStringOptional(details.Disk.Name), + + // Hack because the OpenAPI doesn't specify these fields as return.. I should check this + MountPath: s.WebServiceDetails.Disk.MountPath, + SizeGB: s.WebServiceDetails.Disk.SizeGB, + } + } + docker, err := details.EnvSpecificDetails.AsDockerDetails() + if err == nil { + dockerContext := fromStringOptional(docker.DockerContext) + if dockerContext == types.StringNull() || dockerContext == types.StringValue("") { + dockerContext = s.WebServiceDetails.Docker.DockerContext + } + dockerPath := fromStringOptional(docker.DockerfilePath) + if dockerPath == types.StringNull() || dockerPath == types.StringValue("") { + dockerPath = s.WebServiceDetails.Docker.DockerfilePath + } + service.WebServiceDetails.Docker = &WebServiceDetailsDocker{ + DockerCommand: fromStringOptional(docker.DockerCommand), + DockerContext: dockerContext, + DockerfilePath: dockerPath, + } + } } if serviceType == render.PrivateService { @@ -130,11 +186,49 @@ func (s Service) ToServicePOST(ownerId string) (*render.ServicePOST, error) { service := render.ServicePOST{ Type: serviceType, Name: s.Name.ValueString(), - Repo: s.Repo.ValueString(), + Repo: stringOptionalNil(s.Repo), Branch: stringOptionalNil(s.Branch), OwnerId: ownerId, } + if s.Image != nil { + service.Image = &render.ImagePOST{ + ImagePath: s.Image.ImagePath.ValueString(), + OwnerId: ownerId, + RegistryCredentialId: stringOptionalNil(s.Image.RegistryCredentialId), + } + } + + if s.EnvVars != nil { + var envVars []render.ServicePOST_EnvVars_Item + for _, envVar := range *s.EnvVars { + item := render.ServicePOST_EnvVars_Item{} + if envVar.Generated.ValueBool() { + err := item.FromEnvVarKeyGenerateValue(render.EnvVarKeyGenerateValue{ + Key: envVar.Key.ValueString(), + GenerateValue: "true", + }) + + if err != nil { + return nil, err + } + + } else { + err := item.FromEnvVarKeyValue(render.EnvVarKeyValue{ + Key: envVar.Key.ValueString(), + Value: envVar.Value.ValueString(), + }) + if err != nil { + return nil, err + } + } + + envVars = append(envVars, item) + + } + service.EnvVars = &envVars + } + serviceDetails := render.ServicePOST_ServiceDetails{} if serviceType == render.WebService || s.WebServiceDetails != nil { @@ -324,6 +418,18 @@ func toWebServiceDetails(webServiceDetails *WebServiceDetails) (map[string]inter details["envSpecificDetails"] = native } + if webServiceDetails.Disk != nil { + details["disk"] = toDisk(webServiceDetails.Disk) + } + + if webServiceDetails.Docker != nil { + details["envSpecificDetails"] = map[string]interface{}{ + "dockerCommand": webServiceDetails.Docker.DockerCommand.ValueString(), + "dockerContext": webServiceDetails.Docker.DockerContext.ValueString(), + "dockerfilePath": webServiceDetails.Docker.DockerfilePath.ValueString(), + } + } + return details, nil } @@ -398,6 +504,14 @@ func fromIntOptional(num *int) types.Int64 { return types.Int64Value(int64(*num)) } +func fromBoolOptional(str *render.ServiceAutoDeploy, defaultVal bool) types.Bool { + if str == nil { + return types.BoolValue(defaultVal) + } + + return types.BoolValue(*str == "yes" || *str == "true") +} + func fromStringOptional(str *string) types.String { if str == nil { return types.StringNull() diff --git a/render/resources/service.go b/render/resources/service.go index e17081b..01e7ae9 100644 --- a/render/resources/service.go +++ b/render/resources/service.go @@ -67,6 +67,15 @@ func (r *serviceResource) Schema(_ context.Context, req resource.SchemaRequest, }, } + docker := schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "command": schema.StringAttribute{Optional: true}, + "context": schema.StringAttribute{Optional: true}, + "path": schema.StringAttribute{Optional: true}, + }, + } + resp.Schema = schema.Schema{ Description: `Provider for service resource`, Attributes: map[string]schema.Attribute{ @@ -75,8 +84,28 @@ func (r *serviceResource) Schema(_ context.Context, req resource.SchemaRequest, "type": schema.StringAttribute{Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, "branch": schema.StringAttribute{Optional: true, Computed: true}, "auto_deploy": schema.BoolAttribute{Optional: true}, - "repo": schema.StringAttribute{Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "repo": schema.StringAttribute{Optional: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, "owner": schema.StringAttribute{Optional: true, Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "env_vars": schema.ListNestedAttribute{ + Description: "Service environment variable", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "key": schema.StringAttribute{Required: true}, + "value": schema.StringAttribute{Optional: true, Sensitive: true}, + "generated": schema.BoolAttribute{Optional: true}, + }, + }, + }, + "image": schema.SingleNestedAttribute{ + Description: "Service details for `image` type services.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "image_path": schema.StringAttribute{Required: true}, + "owner_id": schema.StringAttribute{Required: true}, + "registry_credential_id": schema.StringAttribute{Optional: true}, + }, + }, "web_service_details": schema.SingleNestedAttribute{ Description: "Service details for `web_service` type services.", @@ -96,6 +125,8 @@ func (r *serviceResource) Schema(_ context.Context, req resource.SchemaRequest, "start_command": schema.StringAttribute{Optional: true}, }, }, + "disk": disk, + "docker": docker, }, }, diff --git a/render/resources/service_environment.go b/render/resources/service_environment.go index 60ea104..b62feff 100644 --- a/render/resources/service_environment.go +++ b/render/resources/service_environment.go @@ -270,11 +270,11 @@ func (r *serviceEnvironmentResource) Delete(ctx context.Context, req resource.De response, err := r.client.UpdateEnvVarsForServiceWithResponse(ctx, state.Service.ValueString(), variables) if err != nil { - resp.Diagnostics.AddError("failed to update service variables", err.Error()) + resp.Diagnostics.AddError("failed to update service variables1", err.Error()) return } - if response.StatusCode() != http.StatusCreated { + if response.StatusCode() != http.StatusOK { resp.Diagnostics.AddError("failed to update service variables", string(response.Body)) return }