feat: sdk migration in progress

This commit is contained in:
allanice001
2025-11-02 13:19:30 +00:00
commit 0d10d42442
492 changed files with 71067 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
package provider
import (
"context"
"net/http"
"github.com/glueops/autoglue-sdk"
"github.com/hashicorp/terraform-plugin-framework/diag"
)
type Client struct {
SDK *autoglue.APIClient
}
func NewClient(_ context.Context, cfg providerModel) (*Client, diag.Diagnostics) {
var diags diag.Diagnostics
conf := autoglue.NewConfiguration()
conf.Servers = autoglue.ServerConfigurations{{URL: cfg.Addr.ValueString()}}
// Attach auth headers for *every* request
rt := http.DefaultTransport
conf.HTTPClient = &http.Client{
Transport: headerRoundTripper{
under: rt,
bearer: strOrEmpty(cfg.Bearer),
apiKey: strOrEmpty(cfg.APIKey),
orgKey: strOrEmpty(cfg.OrgKey),
orgSecret: strOrEmpty(cfg.OrgSecret),
orgID: strOrEmpty(cfg.OrgID),
},
}
return &Client{SDK: autoglue.NewAPIClient(conf)}, diags
}
type headerRoundTripper struct {
under http.RoundTripper
bearer string
apiKey string
orgKey string
orgSecret string
orgID string
}
func (h headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// Bearer -> Authorization
if h.bearer != "" {
req.Header.Set("Authorization", "Bearer "+h.bearer)
}
// User API Key
if h.apiKey != "" {
req.Header.Set("X-API-KEY", h.apiKey)
}
// Org key/secret
if h.orgKey != "" {
req.Header.Set("X-ORG-KEY", h.orgKey)
}
if h.orgSecret != "" {
req.Header.Set("X-ORG-SECRET", h.orgSecret)
}
// Org selection header (user or key where needed)
if h.orgID != "" {
req.Header.Set("X-Org-ID", h.orgID)
}
return h.under.RoundTrip(req)
}
func strOrEmpty(v interface {
IsNull() bool
IsUnknown() bool
ValueString() string
}) string {
if v.IsNull() || v.IsUnknown() {
return ""
}
return v.ValueString()
}

View File

@@ -0,0 +1,99 @@
package provider
import (
"context"
"os"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/provider"
pschema "github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type providerModel struct {
Addr types.String `tfsdk:"addr"`
Bearer types.String `tfsdk:"bearer"`
APIKey types.String `tfsdk:"api_key"`
OrgKey types.String `tfsdk:"org_key"`
OrgSecret types.String `tfsdk:"org_secret"`
OrgID types.String `tfsdk:"org_id"`
}
func providerConfigSchema() map[string]pschema.Attribute {
return map[string]pschema.Attribute{
"addr": pschema.StringAttribute{
Optional: true,
Description: "Base URL to the autoglue API (e.g. https://gsot.example.com/api/v1). Defaults to http://localhost:8080/api/v1.",
},
"bearer": pschema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Bearer token (user access token).",
},
"api_key": pschema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "User API key for key-only auth.",
},
"org_key": pschema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Org-scoped key for machine auth.",
},
"org_secret": pschema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Org-scoped secret for machine auth.",
},
"org_id": pschema.StringAttribute{
Optional: true,
Description: "Organization ID (UUID). Required for user/bearer and user API key auth unless single-org membership. Omitted for org key/secret (derived server-side).",
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
}
}
func readConfig(ctx context.Context, req provider.ConfigureRequest) (providerModel, diag.Diagnostics) {
var cfg providerModel
var diags diag.Diagnostics
req.Config.Get(ctx, &cfg)
if cfg.Addr.IsNull() || cfg.Addr.IsUnknown() {
if v := os.Getenv("AUTOGLUE_ADDR"); v != "" {
cfg.Addr = types.StringValue(v)
} else {
cfg.Addr = types.StringValue("http://localhost:8080/api/v1")
}
}
if cfg.Bearer.IsNull() || cfg.Bearer.IsUnknown() {
if v := os.Getenv("AUTOGLUE_TOKEN"); v != "" {
cfg.Bearer = types.StringValue(v)
}
}
if cfg.APIKey.IsNull() || cfg.APIKey.IsUnknown() {
if v := os.Getenv("AUTOGLUE_API_KEY"); v != "" {
cfg.APIKey = types.StringValue(v)
}
}
if cfg.OrgKey.IsNull() || cfg.OrgKey.IsUnknown() {
if v := os.Getenv("AUTOGLUE_ORG_KEY"); v != "" {
cfg.OrgKey = types.StringValue(v)
}
}
if cfg.OrgSecret.IsNull() || cfg.OrgSecret.IsUnknown() {
if v := os.Getenv("AUTOGLUE_ORG_SECRET"); v != "" {
cfg.OrgSecret = types.StringValue(v)
}
}
if cfg.OrgID.IsNull() || cfg.OrgID.IsUnknown() {
if v := os.Getenv("AUTOGLUE_ORG_ID"); v != "" {
cfg.OrgID = types.StringValue(v)
} else {
cfg.OrgID = types.StringNull()
}
}
return cfg, diags
}

View File

@@ -0,0 +1,121 @@
package provider
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/datasource"
dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &SshDataSource{}
var _ datasource.DataSourceWithConfigure = &SshDataSource{}
type SshDataSource struct{ client *Client }
func NewSshDataSource() datasource.DataSource { return &SshDataSource{} }
type sshDSModel struct {
NameContains types.String `tfsdk:"name_contains"`
Fingerprint types.String `tfsdk:"fingerprint"`
Keys []sshItem `tfsdk:"keys"`
}
type sshItem struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
PublicKey types.String `tfsdk:"public_key"`
Fingerprint types.String `tfsdk:"fingerprint"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
func (d *SshDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh_keys"
}
func (d *SshDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = dschema.Schema{
Attributes: map[string]dschema.Attribute{
"name_contains": dschema.StringAttribute{
Optional: true,
Description: "Filter by substring of name (client-side).",
},
"fingerprint": dschema.StringAttribute{
Optional: true,
Description: "Filter by exact fingerprint (client-side).",
},
"keys": dschema.ListNestedAttribute{
Computed: true,
Description: "SSH keys",
NestedObject: dschema.NestedAttributeObject{
Attributes: map[string]dschema.Attribute{
"id": dschema.StringAttribute{Computed: true},
"name": dschema.StringAttribute{Computed: true},
"public_key": dschema.StringAttribute{Computed: true},
"fingerprint": dschema.StringAttribute{Computed: true},
"created_at": dschema.StringAttribute{Computed: true},
"updated_at": dschema.StringAttribute{Computed: true},
},
},
},
},
}
}
func (d *SshDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
d.client = req.ProviderData.(*Client)
}
func (d *SshDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
if d.client == nil || d.client.SDK == nil {
resp.Diagnostics.AddError("Client not configured", "Provider configuration missing")
return
}
var conf sshDSModel
resp.Diagnostics.Append(req.Config.Get(ctx, &conf)...)
if resp.Diagnostics.HasError() {
return
}
items, httpResp, err := d.client.SDK.SshAPI.ListPublicSshKeys(ctx).Execute()
if err != nil {
resp.Diagnostics.AddError("List ssh keys failed", fmt.Sprintf("%v", httpErr(err, httpResp)))
return
}
nc := strings.ToLower(conf.NameContains.ValueString())
fp := conf.Fingerprint.ValueString()
out := sshDSModel{NameContains: conf.NameContains, Fingerprint: conf.Fingerprint}
out.Keys = make([]sshItem, 0, len(items))
for _, s := range items {
name := ""
if s.Name != nil {
name = *s.Name
}
if nc != "" && !strings.Contains(strings.ToLower(name), nc) {
continue
}
if fp != "" && (s.Fingerprint == nil || *s.Fingerprint != fp) {
continue
}
out.Keys = append(out.Keys, sshItem{
ID: types.StringPointerValue(s.Id),
Name: types.StringPointerValue(s.Name),
PublicKey: types.StringPointerValue(s.PublicKey),
Fingerprint: types.StringPointerValue(s.Fingerprint),
CreatedAt: types.StringPointerValue(s.CreatedAt),
UpdatedAt: types.StringPointerValue(s.UpdatedAt),
})
}
resp.Diagnostics.Append(resp.State.Set(ctx, &out)...)
}

View File

@@ -0,0 +1,20 @@
package provider
import (
"fmt"
"io"
"net/http"
)
func httpErr(err error, resp *http.Response) string {
if resp == nil {
return err.Error()
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return fmt.Sprintf("status=%d: %s (body=%s)", resp.StatusCode, resp.Status, string(b))
}
func isNotFound(resp *http.Response) bool {
return resp != nil && resp.StatusCode == http.StatusNotFound
}

View File

@@ -0,0 +1,14 @@
package provider
import "math"
func round6(x float64) float64 {
return math.Round(x*1e6) / 1e6
}
func f32ptrToTF64(v *float32) float64 {
if v == nil {
return 0
}
return round6(float64(*v))
}

View File

@@ -0,0 +1,58 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
pschema "github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
var _ provider.Provider = &AutoglueProvider{}
func New() provider.Provider { return &AutoglueProvider{} }
type AutoglueProvider struct {
version string
}
func (p *AutoglueProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "autoglue"
resp.Version = p.version
}
func (p *AutoglueProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = pschema.Schema{
Attributes: providerConfigSchema(),
}
}
func (p *AutoglueProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
cfg, diags := readConfig(ctx, req)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
client, diags := NewClient(ctx, cfg)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *AutoglueProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewSshDataSource,
}
}
func (p *AutoglueProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewSshResource,
}
}

View File

@@ -0,0 +1,230 @@
package provider
import (
"context"
"fmt"
"github.com/glueops/autoglue-sdk"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &SshResource{}
var _ resource.ResourceWithConfigure = &SshResource{}
var _ resource.ResourceWithImportState = &SshResource{}
type SshResource struct{ client *Client }
func NewSshResource() resource.Resource { return &SshResource{} }
type sshResModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Comment types.String `tfsdk:"comment"`
Type types.String `tfsdk:"type"`
Bits types.Int64 `tfsdk:"bits"`
PublicKey types.String `tfsdk:"public_key"`
Fingerprint types.String `tfsdk:"fingerprint"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
PrivateKeyPEM types.String `tfsdk:"private_key_pem"` // not populated by resource
}
func (r *SshResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh_key"
}
func (r *SshResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = rschema.Schema{
Attributes: map[string]rschema.Attribute{
"id": rschema.StringAttribute{
Computed: true,
Description: "SSH key ID (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": rschema.StringAttribute{
Required: true,
Description: "Display name",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"comment": rschema.StringAttribute{
Required: true,
Description: "Comment appended to authorized key",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"type": rschema.StringAttribute{
Optional: true,
Description: "Key type: rsa or ed25519 (default rsa)",
Validators: []validator.String{
stringvalidator.OneOf("rsa", "ed25519", ""),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"bits": rschema.Int64Attribute{
Optional: true,
Description: "RSA key size (2048/3072/4096). Ignored for ed25519.",
Validators: []validator.Int64{
int64validator.OneOf(2048, 3072, 4096),
},
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"public_key": rschema.StringAttribute{
Computed: true,
Description: "OpenSSH authorized key",
},
"fingerprint": rschema.StringAttribute{
Computed: true,
Description: "SHA256 fingerprint",
},
"created_at": rschema.StringAttribute{
Computed: true,
Description: "Creation time (RFC3339, UTC)",
},
"updated_at": rschema.StringAttribute{
Computed: true,
Description: "Update time (RFC3339, UTC)",
},
"private_key_pem": rschema.StringAttribute{
Computed: true,
Sensitive: true,
Description: "Private key PEM (resource doesnt reveal; stays empty).",
},
},
}
}
func (r *SshResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*Client)
}
func (r *SshResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
if r.client == nil || r.client.SDK == nil {
resp.Diagnostics.AddError("Client not configured", "Provider configuration missing")
return
}
var plan sshResModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
body := autoglue.DtoCreateSSHRequest{
Name: plan.Name.ValueStringPointer(),
Comment: plan.Comment.ValueStringPointer(),
}
if t := plan.Type.ValueString(); t != "" {
body.Type = &t
}
if !plan.Bits.IsNull() && !plan.Bits.IsUnknown() {
b := int32(plan.Bits.ValueInt64())
body.Bits = &b
}
created, httpResp, err := r.client.SDK.SshAPI.CreateSSHKey(ctx).Body(body).Execute()
if err != nil {
resp.Diagnostics.AddError("Create ssh key failed", fmt.Sprintf("%v", httpErr(err, httpResp)))
return
}
state := sshResModel{
ID: types.StringPointerValue(created.Id),
Name: types.StringPointerValue(created.Name),
Comment: plan.Comment,
Type: plan.Type,
Bits: plan.Bits,
PublicKey: types.StringPointerValue(created.PublicKey),
Fingerprint: types.StringPointerValue(created.Fingerprint),
CreatedAt: types.StringPointerValue(created.CreatedAt),
UpdatedAt: types.StringPointerValue(created.UpdatedAt),
// PrivateKeyPEM left empty (no reveal on resource)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *SshResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
if r.client == nil || r.client.SDK == nil {
resp.Diagnostics.AddError("Client not configured", "Provider configuration missing")
return
}
var state sshResModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
got, httpResp, err := r.client.SDK.SshAPI.GetSSHKey(ctx, state.ID.ValueString()).Execute()
if err != nil {
if isNotFound(httpResp) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Read ssh key failed", fmt.Sprintf("%v", httpErr(err, httpResp)))
return
}
// Map from flat fields on DtoSshRevealResponse
state.Name = types.StringPointerValue(got.Name)
state.PublicKey = types.StringPointerValue(got.PublicKey)
state.Fingerprint = types.StringPointerValue(got.Fingerprint)
state.CreatedAt = types.StringPointerValue(got.CreatedAt)
state.UpdatedAt = types.StringPointerValue(got.UpdatedAt)
// We intentionally do NOT set PrivateKeyPEM here (resource doesn't reveal)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *SshResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// All changes are RequiresReplace; no server-side update.
var state sshResModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *SshResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
if r.client == nil || r.client.SDK == nil {
resp.Diagnostics.AddError("Client not configured", "Provider configuration missing")
return
}
var state sshResModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
_, httpResp, err := r.client.SDK.SshAPI.DeleteSSHKey(ctx, state.ID.ValueString()).Execute()
if err != nil && !isNotFound(httpResp) {
resp.Diagnostics.AddError("Delete ssh key failed", fmt.Sprintf("%v", httpErr(err, httpResp)))
}
}
func (r *SshResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
}