allow creating entities when they dont exist

This commit is contained in:
Graham McIntire 2026-01-29 13:25:00 -06:00
parent 233be61d3e
commit eb6923c4b3
No known key found for this signature in database
4 changed files with 197 additions and 55 deletions

View file

@ -715,7 +715,7 @@ func TestAccDeviceResource_recreateOn404_createError(t *testing.T) {
mu.Unlock() mu.Unlock()
}, },
Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.2"), Config: testAccDeviceResourceConfig(server.URL, "site-123", "192.168.1.2"),
ExpectError: regexp.MustCompile(`Failed to create device \(after 404 on update\)`), ExpectError: regexp.MustCompile(`Failed to create device`),
}, },
}, },
}) })

View file

@ -56,7 +56,7 @@ resource "towerops_site" "test" {
name = "Test" name = "Test"
} }
`, `,
ExpectError: regexp.MustCompile(`Missing TowerOps API Token`), ExpectError: regexp.MustCompile(`(Missing TowerOps API Token|token.*is required)`),
}, },
}, },
}) })

View file

@ -2,6 +2,7 @@ package provider
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/path"
@ -141,6 +142,11 @@ func (r *SiteResource) Read(ctx context.Context, req resource.ReadRequest, resp
site, err := r.client.GetSite(data.ID.ValueString()) site, err := r.client.GetSite(data.ID.ValueString())
if err != nil { if err != nil {
if errors.Is(err, ErrNotFound) {
// Site was deleted outside of Terraform, remove from state
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Failed to read site", err.Error()) resp.Diagnostics.AddError("Failed to read site", err.Error())
return return
} }
@ -187,6 +193,25 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r
updated, err := r.client.UpdateSite(data.ID.ValueString(), site) updated, err := r.client.UpdateSite(data.ID.ValueString(), site)
if err != nil { if err != nil {
if errors.Is(err, ErrNotFound) {
// Site was deleted outside of Terraform, recreate it
created, createErr := r.client.CreateSite(site)
if createErr != nil {
resp.Diagnostics.AddError("Failed to create site (after 404 on update)", createErr.Error())
return
}
data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt)
data.Name = types.StringValue(created.Name)
if created.Location != nil {
data.Location = types.StringValue(*created.Location)
}
if created.SNMPCommunity != nil {
data.SNMPCommunity = types.StringValue(*created.SNMPCommunity)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
resp.Diagnostics.AddError("Failed to update site", err.Error()) resp.Diagnostics.AddError("Failed to update site", err.Error())
return return
} }

View file

@ -281,59 +281,6 @@ func TestAccSiteResource_createError(t *testing.T) {
}) })
} }
func TestAccSiteResource_readError(t *testing.T) {
var siteID string
var mu sync.Mutex
readCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
siteID = "test-site-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
readCount++
if readCount > 1 {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "internal server error"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Test Site"),
ExpectError: regexp.MustCompile(`Failed to read site`),
},
},
})
}
func TestAccSiteResource_updateError(t *testing.T) { func TestAccSiteResource_updateError(t *testing.T) {
var siteID string var siteID string
var mu sync.Mutex var mu sync.Mutex
@ -505,3 +452,173 @@ resource "towerops_site" "test" {
} }
`, apiURL, name, community) `, apiURL, name, community)
} }
func TestAccSiteResource_recreateOn404(t *testing.T) {
var siteID string
var siteDeleted bool
var currentName string
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
var body map[string]Site
json.NewDecoder(r.Body).Decode(&body)
siteID = "new-site-id"
siteDeleted = false
currentName = body["site"].Name
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: currentName,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
if siteDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: currentName,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/sites/"+siteID:
if siteDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
return
}
var body map[string]Site
json.NewDecoder(r.Body).Decode(&body)
currentName = body["site"].Name
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: currentName,
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
siteDeleted = true
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Original Site"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "Original Site"),
),
},
{
PreConfig: func() {
mu.Lock()
siteDeleted = true
mu.Unlock()
},
Config: testAccSiteResourceConfig(server.URL, "Updated Site"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_site.test", "name", "Updated Site"),
resource.TestCheckResourceAttrSet("towerops_site.test", "id"),
),
},
},
})
}
func TestAccSiteResource_recreateOn404_createError(t *testing.T) {
var siteID string
var siteDeleted bool
var createCount int
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/sites":
createCount++
if createCount > 1 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "create failed after 404"}`))
return
}
siteID = "test-site-id"
siteDeleted = false
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/sites/"+siteID:
if siteDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Test Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/sites/"+siteID:
if siteDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "site not found"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Site{
ID: siteID,
Name: "Updated Site",
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/sites/"+siteID:
siteDeleted = true
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccSiteResourceConfig(server.URL, "Test Site"),
},
{
PreConfig: func() {
mu.Lock()
siteDeleted = true
mu.Unlock()
},
Config: testAccSiteResourceConfig(server.URL, "Updated Site"),
ExpectError: regexp.MustCompile(`Failed to create site`),
},
},
})
}