diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fb9003..a99f28f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,8 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/hypeman-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/hypeman-go' && - (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && + (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 diff --git a/.gitignore b/.gitignore index c6d0501..8554aff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log codegen.log Brewfile.lock.json .idea/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b4e9013..6db19b9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.16.0" + ".": "0.17.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 4d77a66..327b3e8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 47 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-8251e13b6a8ec8965b5cb0b5ab33d4a16e274a4be7cd6d9fa36642878108797c.yml -openapi_spec_hash: 5c1c0d21d430074ffa76ae62ea137f0b -config_hash: 47cce606a7f8af4dac9c2a8dbc822484 +configured_endpoints: 50 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-c642f2375b218db533c8ae0ff0695b85c048c72fe19400788ecd2c9d992def46.yml +openapi_spec_hash: 505818e6d24972006fa869dd6ed90757 +config_hash: 32c7f371b192b03881971de768ebb07d diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2ab44..2c100e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.17.0 (2026-03-25) + +Full Changelog: [v0.16.0...v0.17.0](https://github.com/kernel/hypeman-go/compare/v0.16.0...v0.17.0) + +### Features + +* Add scheduled instance snapshots with retention cleanup ([f0d4d52](https://github.com/kernel/hypeman-go/commit/f0d4d52bf563ca828cba5f31f6a0c913d4dba4f8)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([e3c8b1f](https://github.com/kernel/hypeman-go/commit/e3c8b1f0943858b91f196db4889a27f0d626a6e4)) +* **internal:** update gitignore ([3dbe64e](https://github.com/kernel/hypeman-go/commit/3dbe64e2bdb18744c58d30a8a9df44107fed6a61)) + ## 0.16.0 (2026-03-23) Full Changelog: [v0.15.0...v0.16.0](https://github.com/kernel/hypeman-go/compare/v0.15.0...v0.16.0) diff --git a/README.md b/README.md index 62dd1ed..e3d4666 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/kernel/hypeman-go@v0.16.0' +go get -u 'github.com/kernel/hypeman-go@v0.17.0' ``` diff --git a/api.md b/api.md index 01ab836..4c97e0f 100644 --- a/api.md +++ b/api.md @@ -33,7 +33,9 @@ Methods: Params Types: +- hypeman.SetSnapshotScheduleRequestParam - hypeman.SnapshotPolicyParam +- hypeman.SnapshotScheduleRetentionParam - hypeman.VolumeMountParam Response Types: @@ -42,6 +44,8 @@ Response Types: - hypeman.InstanceStats - hypeman.PathInfo - hypeman.SnapshotPolicy +- hypeman.SnapshotSchedule +- hypeman.SnapshotScheduleRetention - hypeman.VolumeMount Methods: @@ -74,6 +78,14 @@ Methods: - client.Instances.Snapshots.New(ctx context.Context, id string, body hypeman.InstanceSnapshotNewParams) (\*hypeman.Snapshot, error) - client.Instances.Snapshots.Restore(ctx context.Context, snapshotID string, params hypeman.InstanceSnapshotRestoreParams) (\*hypeman.Instance, error) +## SnapshotSchedule + +Methods: + +- client.Instances.SnapshotSchedule.Update(ctx context.Context, id string, body hypeman.InstanceSnapshotScheduleUpdateParams) (\*hypeman.SnapshotSchedule, error) +- client.Instances.SnapshotSchedule.Delete(ctx context.Context, id string) error +- client.Instances.SnapshotSchedule.Get(ctx context.Context, id string) (\*hypeman.SnapshotSchedule, error) + # Snapshots Params Types: diff --git a/instance.go b/instance.go index a2c146f..a9244e4 100644 --- a/instance.go +++ b/instance.go @@ -29,9 +29,10 @@ import ( // automatically. You should not instantiate this service directly, and instead use // the [NewInstanceService] method instead. type InstanceService struct { - Options []option.RequestOption - Volumes InstanceVolumeService - Snapshots InstanceSnapshotService + Options []option.RequestOption + Volumes InstanceVolumeService + Snapshots InstanceSnapshotService + SnapshotSchedule InstanceSnapshotScheduleService } // NewInstanceService generates a new service that applies the given options to @@ -42,6 +43,7 @@ func NewInstanceService(opts ...option.RequestOption) (r InstanceService) { r.Options = opts r.Volumes = NewInstanceVolumeService(opts...) r.Snapshots = NewInstanceSnapshotService(opts...) + r.SnapshotSchedule = NewInstanceSnapshotScheduleService(opts...) return } @@ -481,6 +483,27 @@ func (r *PathInfo) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// The properties Interval, Retention are required. +type SetSnapshotScheduleRequestParam struct { + // Snapshot interval (Go duration format, minimum 1m). + Interval string `json:"interval" api:"required"` + // At least one of max_count or max_age must be provided. + Retention SnapshotScheduleRetentionParam `json:"retention,omitzero" api:"required"` + // Optional prefix for auto-generated scheduled snapshot names (max 47 chars). + NamePrefix param.Opt[string] `json:"name_prefix,omitzero"` + // User-defined key-value tags. + Metadata map[string]string `json:"metadata,omitzero"` + paramObj +} + +func (r SetSnapshotScheduleRequestParam) MarshalJSON() (data []byte, err error) { + type shadow SetSnapshotScheduleRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *SetSnapshotScheduleRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type SnapshotPolicy struct { Compression shared.SnapshotCompressionConfig `json:"compression"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -519,6 +542,103 @@ func (r *SnapshotPolicyParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type SnapshotSchedule struct { + // Schedule creation timestamp. + CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` + // Source instance ID. + InstanceID string `json:"instance_id" api:"required"` + // Snapshot interval (Go duration format). + Interval string `json:"interval" api:"required"` + // Next scheduled run time. + NextRunAt time.Time `json:"next_run_at" api:"required" format:"date-time"` + // Automatic cleanup policy for scheduled snapshots. + Retention SnapshotScheduleRetention `json:"retention" api:"required"` + // Schedule update timestamp. + UpdatedAt time.Time `json:"updated_at" api:"required" format:"date-time"` + // Last schedule run error, if any. + LastError string `json:"last_error" api:"nullable"` + // Last schedule execution time. + LastRunAt time.Time `json:"last_run_at" api:"nullable" format:"date-time"` + // Snapshot ID produced by the last successful run. + LastSnapshotID string `json:"last_snapshot_id" api:"nullable"` + // User-defined key-value tags. + Metadata map[string]string `json:"metadata"` + // Optional prefix used for generated scheduled snapshot names. + NamePrefix string `json:"name_prefix" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + CreatedAt respjson.Field + InstanceID respjson.Field + Interval respjson.Field + NextRunAt respjson.Field + Retention respjson.Field + UpdatedAt respjson.Field + LastError respjson.Field + LastRunAt respjson.Field + LastSnapshotID respjson.Field + Metadata respjson.Field + NamePrefix respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r SnapshotSchedule) RawJSON() string { return r.JSON.raw } +func (r *SnapshotSchedule) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Automatic cleanup policy for scheduled snapshots. +type SnapshotScheduleRetention struct { + // Delete scheduled snapshots older than this duration (Go duration format). + MaxAge string `json:"max_age"` + // Keep at most this many scheduled snapshots for the instance (0 disables + // count-based cleanup). + MaxCount int64 `json:"max_count"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + MaxAge respjson.Field + MaxCount respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r SnapshotScheduleRetention) RawJSON() string { return r.JSON.raw } +func (r *SnapshotScheduleRetention) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// ToParam converts this SnapshotScheduleRetention to a +// SnapshotScheduleRetentionParam. +// +// Warning: the fields of the param type will not be present. ToParam should only +// be used at the last possible moment before sending a request. Test for this with +// SnapshotScheduleRetentionParam.Overrides() +func (r SnapshotScheduleRetention) ToParam() SnapshotScheduleRetentionParam { + return param.Override[SnapshotScheduleRetentionParam](json.RawMessage(r.RawJSON())) +} + +// Automatic cleanup policy for scheduled snapshots. +type SnapshotScheduleRetentionParam struct { + // Delete scheduled snapshots older than this duration (Go duration format). + MaxAge param.Opt[string] `json:"max_age,omitzero"` + // Keep at most this many scheduled snapshots for the instance (0 disables + // count-based cleanup). + MaxCount param.Opt[int64] `json:"max_count,omitzero"` + paramObj +} + +func (r SnapshotScheduleRetentionParam) MarshalJSON() (data []byte, err error) { + type shadow SnapshotScheduleRetentionParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *SnapshotScheduleRetentionParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type VolumeMount struct { // Path where volume is mounted in the guest MountPath string `json:"mount_path" api:"required"` diff --git a/instancesnapshotschedule.go b/instancesnapshotschedule.go new file mode 100644 index 0000000..44d9966 --- /dev/null +++ b/instancesnapshotschedule.go @@ -0,0 +1,93 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "slices" + + shimjson "github.com/kernel/hypeman-go/internal/encoding/json" + "github.com/kernel/hypeman-go/internal/requestconfig" + "github.com/kernel/hypeman-go/option" +) + +// InstanceSnapshotScheduleService contains methods and other services that help +// with interacting with the hypeman API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewInstanceSnapshotScheduleService] method instead. +type InstanceSnapshotScheduleService struct { + Options []option.RequestOption +} + +// NewInstanceSnapshotScheduleService generates a new service that applies the +// given options to each request. These options are applied after the parent +// client's options (if there is one), and before any request-specific options. +func NewInstanceSnapshotScheduleService(opts ...option.RequestOption) (r InstanceSnapshotScheduleService) { + r = InstanceSnapshotScheduleService{} + r.Options = opts + return +} + +// Scheduled runs automatically choose snapshot behavior from current instance +// state: +// +// - `Running` or `Standby` source: create a `Standby` snapshot. +// - `Stopped` source: create a `Stopped` snapshot. For running instances, this +// includes a brief pause/resume cycle during each capture. The minimum supported +// interval is `1m`, but larger intervals are recommended for heavier or +// latency-sensitive workloads. Updating only retention, metadata, or +// `name_prefix` preserves the next scheduled run; changing `interval` +// establishes a new cadence. +func (r *InstanceSnapshotScheduleService) Update(ctx context.Context, id string, body InstanceSnapshotScheduleUpdateParams, opts ...option.RequestOption) (res *SnapshotSchedule, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("instances/%s/snapshot-schedule", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...) + return res, err +} + +// Delete snapshot schedule for an instance +func (r *InstanceSnapshotScheduleService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) + if id == "" { + err = errors.New("missing required id parameter") + return err + } + path := fmt.Sprintf("instances/%s/snapshot-schedule", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return err +} + +// Get snapshot schedule for an instance +func (r *InstanceSnapshotScheduleService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *SnapshotSchedule, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("instances/%s/snapshot-schedule", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return res, err +} + +type InstanceSnapshotScheduleUpdateParams struct { + SetSnapshotScheduleRequest SetSnapshotScheduleRequestParam + paramObj +} + +func (r InstanceSnapshotScheduleUpdateParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.SetSnapshotScheduleRequest) +} +func (r *InstanceSnapshotScheduleUpdateParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.SetSnapshotScheduleRequest) +} diff --git a/instancesnapshotschedule_test.go b/instancesnapshotschedule_test.go new file mode 100644 index 0000000..13337ec --- /dev/null +++ b/instancesnapshotschedule_test.go @@ -0,0 +1,100 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/internal/testutil" + "github.com/kernel/hypeman-go/option" +) + +func TestInstanceSnapshotScheduleUpdateWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.SnapshotSchedule.Update( + context.TODO(), + "id", + hypeman.InstanceSnapshotScheduleUpdateParams{ + SetSnapshotScheduleRequest: hypeman.SetSnapshotScheduleRequestParam{ + Interval: "24h", + Retention: hypeman.SnapshotScheduleRetentionParam{ + MaxAge: hypeman.String("168h"), + MaxCount: hypeman.Int(7), + }, + Metadata: map[string]string{ + "team": "backend", + "env": "staging", + }, + NamePrefix: hypeman.String("nightly"), + }, + }, + ) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInstanceSnapshotScheduleDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + err := client.Instances.SnapshotSchedule.Delete(context.TODO(), "id") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestInstanceSnapshotScheduleGet(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.SnapshotSchedule.Get(context.TODO(), "id") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/internal/version.go b/internal/version.go index 16d4c3d..c265c64 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.16.0" // x-release-please-version +const PackageVersion = "0.17.0" // x-release-please-version