feat(members): add governed Cluster client (#23)

agent/track-event-client-v2 v0.7.18
Vernon Keenan 2026-07-23 23:55:47 -07:00 committed by GitHub
parent bdf29af51a
commit d53ad0d601
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1167 additions and 69 deletions

View File

@ -0,0 +1,305 @@
package members_client_test
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
"code.tnxs.net/vernonkeenan/lib/api/members/members_client"
"code.tnxs.net/vernonkeenan/lib/api/members/members_client/clusters"
"code.tnxs.net/vernonkeenan/lib/api/members/members_models"
openapiruntime "github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
)
var clusterTestAuth = openapiruntime.ClientAuthInfoWriterFunc(
func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil },
)
type clusterCaptureTransport struct {
operation *openapiruntime.ClientOperation
}
func (transport *clusterCaptureTransport) Submit(
operation *openapiruntime.ClientOperation,
) (any, error) {
return transport.SubmitContext(context.Background(), operation)
}
func (transport *clusterCaptureTransport) SubmitContext(
_ context.Context,
operation *openapiruntime.ClientOperation,
) (any, error) {
transport.operation = operation
switch operation.ID {
case "getClusters":
return clusters.NewGetClustersOK(), nil
case "postClusters":
return clusters.NewPostClustersOK(), nil
case "putClusters":
return clusters.NewPutClustersOK(), nil
default:
panic("unexpected generated Cluster operation: " + operation.ID)
}
}
func TestGeneratedClusterOperationContract(t *testing.T) {
type operationCall func(openapiruntime.ContextualTransport) error
tests := []struct {
name, wantID, wantMethod, wantPath string
call operationCall
}{
{
name: "list or get Cluster", wantID: "getClusters",
wantMethod: "GET", wantPath: "/clusters",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := clusters.New(transport, strfmt.Default).GetClusters(
clusters.NewGetClustersParams().WithTenantID("tenant-example"),
clusterTestAuth,
)
return err
},
},
{
name: "create Cluster", wantID: "postClusters",
wantMethod: "POST", wantPath: "/clusters",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := clusters.New(transport, strfmt.Default).PostClusters(
clusters.NewPostClustersParams().WithTenantID("tenant-example"),
clusterTestAuth,
)
return err
},
},
{
name: "CAS update Cluster", wantID: "putClusters",
wantMethod: "PUT", wantPath: "/clusters",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := clusters.New(transport, strfmt.Default).PutClusters(
clusters.NewPutClustersParams().WithTenantID("tenant-example"),
clusterTestAuth,
)
return err
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
transport := &clusterCaptureTransport{}
if err := test.call(transport); err != nil {
t.Fatalf("generated client operation failed: %v", err)
}
if transport.operation == nil {
t.Fatal("generated client did not submit an operation")
}
if transport.operation.ID != test.wantID {
t.Errorf("operation ID = %q, want %q", transport.operation.ID, test.wantID)
}
if transport.operation.Method != test.wantMethod {
t.Errorf("method = %q, want %q", transport.operation.Method, test.wantMethod)
}
if transport.operation.PathPattern != test.wantPath {
t.Errorf("path = %q, want %q", transport.operation.PathPattern, test.wantPath)
}
if transport.operation.AuthInfo == nil {
t.Error("generated operation discarded its compound auth-info writer")
}
})
}
}
func TestClusterTenantBindingCompoundAuthExactScopesAndCAS(t *testing.T) {
specText := readMembersLearningSpec(t)
pathBlock := learningSpecPathBlock(t, specText, "/clusters")
for _, forbidden := range []string{" delete:", " patch:"} {
if strings.Contains(pathBlock, forbidden) {
t.Errorf("/clusters exposes unsupported %s", strings.TrimSpace(forbidden))
}
}
operations := map[string]struct {
id, scope string
}{
"get": {id: "getClusters", scope: "members:cluster:read"},
"post": {id: "postClusters", scope: "members:cluster:create"},
"put": {id: "putClusters", scope: "members:cluster:update"},
}
const compoundSecurity = " security:\n - ApiKeyAuth: []\n kvSessionCookie: []"
for method, expected := range operations {
block := learningSpecOperationBlock(t, specText, "/clusters", method)
if !strings.Contains(block, " operationId: "+expected.id+"\n") {
t.Errorf("%s /clusters lost operationId %q", strings.ToUpper(method), expected.id)
}
if strings.Count(block, compoundSecurity) != 1 {
t.Errorf("%s /clusters does not preserve compound authentication", strings.ToUpper(method))
}
if !strings.Contains(block, expected.scope) {
t.Errorf("%s /clusters lost exact scope %q", strings.ToUpper(method), expected.scope)
}
if !strings.Contains(block, `- $ref: "#/parameters/clusterTenantId"`) {
t.Errorf("%s /clusters is not tenantId-bound", strings.ToUpper(method))
}
}
recordID := "cluster-id"
getParams := clusters.NewGetClustersParams().
WithTenantID("tenant-example").
WithClusterID(&recordID)
if getParams.TenantID != "tenant-example" {
t.Fatal("Cluster list/get client lost the required tenantId")
}
if getParams.ClusterID == nil || *getParams.ClusterID != recordID {
t.Fatal("Cluster list/get client lost its clusterId filter")
}
if clusters.NewPostClustersParams().WithTenantID("tenant-example").TenantID != "tenant-example" {
t.Fatal("Cluster create client lost the required tenantId")
}
if clusters.NewPutClustersParams().WithTenantID("tenant-example").TenantID != "tenant-example" {
t.Fatal("Cluster update client lost the required tenantId")
}
if !clusters.NewPutClustersConflict().IsCode(409) {
t.Fatal("Cluster update client lost the optimistic concurrency response")
}
}
func TestClusterModelAndSingleRecordRequestAreBounded(t *testing.T) {
assertExactModelFields(t, members_models.Cluster{}, map[string]string{
"CreatedByID": "*string", "CreatedDate": "*string", "Description": "*string",
"Environment": "*string", "Gateway": "*string", "ID": "string",
"IPAddress": "*string", "LastModifiedByID": "*string",
"LastModifiedDate": "*string", "Name": "*string", "OwnerID": "*string",
"Ref": "*string", "Status": "*string", "Subnet": "*string",
"TenantID": "*string", "Type": "*string", "Zone": "*string",
})
firstName, secondName := "One", "Two"
request := &members_models.ClusterRequest{
Data: []*members_models.Cluster{{Name: &firstName}, {Name: &secondName}},
}
if err := request.Validate(strfmt.Default); err == nil {
t.Fatal("generated Cluster request accepted a bulk payload")
}
if err := (&members_models.ClusterRequest{
Data: []*members_models.Cluster{{Name: &firstName}},
}).Validate(strfmt.Default); err != nil {
t.Fatalf("generated Cluster request rejected one valid record: %v", err)
}
}
func TestMembersCompoundAuthWritesBothCredentialsWithoutExposure(t *testing.T) {
const (
apiKey = "test-api-key"
sessionToken = "test-session-token"
)
writer, err := members_client.NewMembersCompoundAuth(apiKey, sessionToken)
if err != nil {
t.Fatalf("construct compound auth writer: %v", err)
}
request := new(openapiruntime.TestClientRequest)
if err := writer.AuthenticateRequest(request, strfmt.Default); err != nil {
t.Fatalf("write compound authentication: %v", err)
}
if got := request.Headers.Get("X-API-Key"); got != apiKey {
t.Errorf("X-API-Key = %q, want test value", got)
}
if got := request.Headers.Get("Cookie"); got != "kvSession="+sessionToken {
t.Errorf("Cookie = %q, want kvSession test value", got)
}
formatted := fmt.Sprintf("%v", writer)
if strings.Contains(formatted, apiKey) || strings.Contains(formatted, sessionToken) {
t.Fatal("compound auth writer formatting exposed credential material")
}
}
func TestMembersCompoundAuthRejectsMissingOrUnsafeCredentialsWithoutEcho(t *testing.T) {
tests := []struct {
name, apiKey, sessionToken string
}{
{name: "missing API key", sessionToken: "session"},
{name: "missing session", apiKey: "api-key"},
{name: "API key header injection", apiKey: "api-key\r\nunsafe", sessionToken: "session"},
{name: "session cookie injection", apiKey: "api-key", sessionToken: "session;unsafe"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if writer, err := members_client.NewMembersCompoundAuth(
test.apiKey, test.sessionToken,
); err == nil || writer != nil {
t.Fatal("unsafe compound authentication was accepted")
} else if (test.apiKey != "" && strings.Contains(err.Error(), test.apiKey)) ||
(test.sessionToken != "" && strings.Contains(err.Error(), test.sessionToken)) {
t.Fatal("validation error exposed credential material")
}
})
}
}
func TestClusterSurfaceOmitsDeleteDatabaseAndDSN(t *testing.T) {
contract := reflect.TypeOf((*clusters.ClientService)(nil)).Elem()
for index := 0; index < contract.NumMethod(); index++ {
method := contract.Method(index).Name
for _, forbidden := range []string{"Delete", "Database", "DSN"} {
if strings.Contains(method, forbidden) {
t.Errorf("Cluster client exposes forbidden method %s", method)
}
}
}
root := reflect.TypeOf(members_client.Members{})
if _, exists := root.FieldByName("Clusters"); !exists {
t.Fatal("root Members client does not expose governed Clusters")
}
}
func TestGeneratedClusterSurfaceHasNoExternalBypassOrSecrets(t *testing.T) {
repoRoot := learningRepoRoot(t)
targets := []string{
filepath.Join(repoRoot, "api", "members", "members_client", "clusters"),
filepath.Join(repoRoot, "api", "members", "members_client", "compound_auth.go"),
filepath.Join(repoRoot, "api", "members", "members_models", "cluster.go"),
filepath.Join(repoRoot, "api", "members", "members_models", "cluster_request.go"),
filepath.Join(repoRoot, "api", "members", "members_models", "cluster_response.go"),
}
credentialLiteral := regexp.MustCompile(
`(?i)(api[_-]?key|password|secret)\s*[:=]\s*["'][^"']+["']`,
)
for _, target := range targets {
err := filepath.WalkDir(target, func(
path string, entry os.DirEntry, walkErr error,
) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() || !strings.HasSuffix(path, ".go") ||
strings.HasSuffix(path, "_test.go") {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
lower := strings.ToLower(string(content))
for _, forbidden := range []string{
"salesforce", "sf-gate", "go-force", "private key-----",
"members_client/databases", "dsn",
} {
if strings.Contains(lower, forbidden) {
t.Errorf("%s contains forbidden boundary %q", path, forbidden)
}
}
if credentialLiteral.Match(content) {
t.Errorf("%s contains a credential-shaped literal", path)
}
return nil
})
if err != nil {
t.Fatalf("scan generated Cluster target %s: %v", target, err)
}
}
}

View File

@ -60,30 +60,30 @@ type ClientOption func(*runtime.ClientOperation)
// ClientService is the interface for Client methods. // ClientService is the interface for Client methods.
type ClientService interface { type ClientService interface {
// GetClusters get a list clusters. // GetClusters get governed tenant clusters.
GetClusters(params *GetClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetClustersOK, error) GetClusters(params *GetClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetClustersOK, error)
// GetClustersContext get a list clusters. // GetClustersContext get governed tenant clusters.
GetClustersContext(ctx context.Context, params *GetClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetClustersOK, error) GetClustersContext(ctx context.Context, params *GetClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetClustersOK, error)
// PostClusters create new clusters. // PostClusters create a governed cluster.
PostClusters(params *PostClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostClustersOK, error) PostClusters(params *PostClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostClustersOK, error)
// PostClustersContext create new clusters. // PostClustersContext create a governed cluster.
PostClustersContext(ctx context.Context, params *PostClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostClustersOK, error) PostClustersContext(ctx context.Context, params *PostClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostClustersOK, error)
// PutClusters update clustera. // PutClusters update a governed cluster.
PutClusters(params *PutClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutClustersOK, error) PutClusters(params *PutClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutClustersOK, error)
// PutClustersContext update clustera. // PutClustersContext update a governed cluster.
PutClustersContext(ctx context.Context, params *PutClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutClustersOK, error) PutClustersContext(ctx context.Context, params *PutClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutClustersOK, error)
SetTransport(transport runtime.ContextualTransport) SetTransport(transport runtime.ContextualTransport)
} }
// GetClusters gets a list clusters. // GetClusters gets governed tenant clusters.
// //
// Return a list of Cluster records from the datastore. // Return tenant-scoped Cluster records. Requires members:cluster:read and an active human membership in tenantId..
// //
// This method does not support injected context. // This method does not support injected context.
// However, timeout and opentracing contexts are honored whenever enabled. // However, timeout and opentracing contexts are honored whenever enabled.
@ -100,9 +100,9 @@ func (a *Client) GetClusters(params *GetClustersParams, authInfo runtime.ClientA
return a.GetClustersContext(ctx, params, authInfo, opts...) return a.GetClustersContext(ctx, params, authInfo, opts...)
} }
// GetClustersContext gets a list clusters. // GetClustersContext gets governed tenant clusters.
// //
// Return a list of Cluster records from the datastore. // Return tenant-scoped Cluster records. Requires members:cluster:read and an active human membership in tenantId..
// //
// Do not use the deprecated [GetClustersParams.Context] with this method: it would be ignored. // Do not use the deprecated [GetClustersParams.Context] with this method: it would be ignored.
func (a *Client) GetClustersContext(ctx context.Context, params *GetClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetClustersOK, error) { func (a *Client) GetClustersContext(ctx context.Context, params *GetClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetClustersOK, error) {
@ -148,9 +148,9 @@ func (a *Client) GetClustersContext(ctx context.Context, params *GetClustersPara
panic(msg) panic(msg)
} }
// PostClusters creates new clusters. // PostClusters creates a governed cluster.
// //
// Create Clusters. // Create one tenant-scoped Cluster. Requires members:cluster:create and active Owner or Manager access. ID, TenantID, and audit fields are server-owned..
// //
// This method does not support injected context. // This method does not support injected context.
// However, timeout and opentracing contexts are honored whenever enabled. // However, timeout and opentracing contexts are honored whenever enabled.
@ -167,9 +167,9 @@ func (a *Client) PostClusters(params *PostClustersParams, authInfo runtime.Clien
return a.PostClustersContext(ctx, params, authInfo, opts...) return a.PostClustersContext(ctx, params, authInfo, opts...)
} }
// PostClustersContext creates new clusters. // PostClustersContext creates a governed cluster.
// //
// Create Clusters. // Create one tenant-scoped Cluster. Requires members:cluster:create and active Owner or Manager access. ID, TenantID, and audit fields are server-owned..
// //
// Do not use the deprecated [PostClustersParams.Context] with this method: it would be ignored. // Do not use the deprecated [PostClustersParams.Context] with this method: it would be ignored.
func (a *Client) PostClustersContext(ctx context.Context, params *PostClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostClustersOK, error) { func (a *Client) PostClustersContext(ctx context.Context, params *PostClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostClustersOK, error) {
@ -215,9 +215,9 @@ func (a *Client) PostClustersContext(ctx context.Context, params *PostClustersPa
panic(msg) panic(msg)
} }
// PutClusters updates clustera. // PutClusters updates a governed cluster.
// //
// Update Cluster. // CAS-update one tenant-scoped Cluster. Requires members:cluster:update, active Owner or Manager access, and the current LastModifiedDate. TenantID and audit fields remain server-owned..
// //
// This method does not support injected context. // This method does not support injected context.
// However, timeout and opentracing contexts are honored whenever enabled. // However, timeout and opentracing contexts are honored whenever enabled.
@ -234,9 +234,9 @@ func (a *Client) PutClusters(params *PutClustersParams, authInfo runtime.ClientA
return a.PutClustersContext(ctx, params, authInfo, opts...) return a.PutClustersContext(ctx, params, authInfo, opts...)
} }
// PutClustersContext updates clustera. // PutClustersContext updates a governed cluster.
// //
// Update Cluster. // CAS-update one tenant-scoped Cluster. Requires members:cluster:update, active Owner or Manager access, and the current LastModifiedDate. TenantID and audit fields remain server-owned..
// //
// Do not use the deprecated [PutClustersParams.Context] with this method: it would be ignored. // Do not use the deprecated [PutClustersParams.Context] with this method: it would be ignored.
func (a *Client) PutClustersContext(ctx context.Context, params *PutClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutClustersOK, error) { func (a *Client) PutClustersContext(ctx context.Context, params *PutClustersParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutClustersOK, error) {

View File

@ -86,6 +86,11 @@ type GetClustersParams struct {
// Format: int64 // Format: int64
Offset *int64 Offset *int64
// TenantID.
//
// Tenant that owns the Cluster. Members verifies the active human membership and never accepts TenantID from the body.
TenantID string
HTTPClient *http.Client HTTPClient *http.Client
inner innerParams inner innerParams
@ -176,6 +181,17 @@ func (o *GetClustersParams) SetOffset(offset *int64) {
o.Offset = offset o.Offset = offset
} }
// WithTenantID adds the tenantID to the get clusters params.
func (o *GetClustersParams) WithTenantID(tenantID string) *GetClustersParams {
o.SetTenantID(tenantID)
return o
}
// SetTenantID adds the tenantId to the get clusters params.
func (o *GetClustersParams) SetTenantID(tenantID string) {
o.TenantID = tenantID
}
// WriteToRequest writes these params to a [runtime.ClientRequest]. // WriteToRequest writes these params to a [runtime.ClientRequest].
func (o *GetClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { func (o *GetClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.inner.timeout); err != nil { if err := r.SetTimeout(o.inner.timeout); err != nil {
@ -234,6 +250,16 @@ func (o *GetClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.R
} }
} }
// query param tenantId
qrTenantID := o.TenantID
qTenantID := qrTenantID
if qTenantID != "" {
if err := r.SetQueryParam("tenantId", qTenantID); err != nil {
return err
}
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }

View File

@ -69,9 +69,14 @@ type PostClustersParams struct {
// ClusterRequest. // ClusterRequest.
// //
// An array of Cluster records // Exactly one governed Cluster record
ClusterRequest *members_models.ClusterRequest ClusterRequest *members_models.ClusterRequest
// TenantID.
//
// Tenant that owns the Cluster. Members verifies the active human membership and never accepts TenantID from the body.
TenantID string
HTTPClient *http.Client HTTPClient *http.Client
inner innerParams inner innerParams
@ -140,6 +145,17 @@ func (o *PostClustersParams) SetClusterRequest(clusterRequest *members_models.Cl
o.ClusterRequest = clusterRequest o.ClusterRequest = clusterRequest
} }
// WithTenantID adds the tenantID to the post clusters params.
func (o *PostClustersParams) WithTenantID(tenantID string) *PostClustersParams {
o.SetTenantID(tenantID)
return o
}
// SetTenantID adds the tenantId to the post clusters params.
func (o *PostClustersParams) SetTenantID(tenantID string) {
o.TenantID = tenantID
}
// WriteToRequest writes these params to a [runtime.ClientRequest]. // WriteToRequest writes these params to a [runtime.ClientRequest].
func (o *PostClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { func (o *PostClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.inner.timeout); err != nil { if err := r.SetTimeout(o.inner.timeout); err != nil {
@ -152,6 +168,16 @@ func (o *PostClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.
} }
} }
// query param tenantId
qrTenantID := o.TenantID
qTenantID := qrTenantID
if qTenantID != "" {
if err := r.SetQueryParam("tenantId", qTenantID); err != nil {
return err
}
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }

View File

@ -69,9 +69,14 @@ type PutClustersParams struct {
// ClusterRequest. // ClusterRequest.
// //
// An array of Cluster records // Exactly one governed Cluster record
ClusterRequest *members_models.ClusterRequest ClusterRequest *members_models.ClusterRequest
// TenantID.
//
// Tenant that owns the Cluster. Members verifies the active human membership and never accepts TenantID from the body.
TenantID string
HTTPClient *http.Client HTTPClient *http.Client
inner innerParams inner innerParams
@ -140,6 +145,17 @@ func (o *PutClustersParams) SetClusterRequest(clusterRequest *members_models.Clu
o.ClusterRequest = clusterRequest o.ClusterRequest = clusterRequest
} }
// WithTenantID adds the tenantID to the put clusters params.
func (o *PutClustersParams) WithTenantID(tenantID string) *PutClustersParams {
o.SetTenantID(tenantID)
return o
}
// SetTenantID adds the tenantId to the put clusters params.
func (o *PutClustersParams) SetTenantID(tenantID string) {
o.TenantID = tenantID
}
// WriteToRequest writes these params to a [runtime.ClientRequest]. // WriteToRequest writes these params to a [runtime.ClientRequest].
func (o *PutClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { func (o *PutClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.inner.timeout); err != nil { if err := r.SetTimeout(o.inner.timeout); err != nil {
@ -152,6 +168,16 @@ func (o *PutClustersParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.R
} }
} }
// query param tenantId
qrTenantID := o.TenantID
qTenantID := qrTenantID
if qTenantID != "" {
if err := r.SetQueryParam("tenantId", qTenantID); err != nil {
return err
}
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }

View File

@ -49,6 +49,12 @@ func (o *PutClustersReader) ReadResponse(response runtime.ClientResponse, consum
return nil, err return nil, err
} }
return nil, result return nil, result
case 409:
result := NewPutClustersConflict()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 422: case 422:
result := NewPutClustersUnprocessableEntity() result := NewPutClustersUnprocessableEntity()
if err := result.readResponse(response, consumer, o.formats); err != nil { if err := result.readResponse(response, consumer, o.formats); err != nil {
@ -347,6 +353,74 @@ func (o *PutClustersNotFound) readResponse(response runtime.ClientResponse, cons
return nil return nil
} }
// NewPutClustersConflict creates a PutClustersConflict with default headers values
func NewPutClustersConflict() *PutClustersConflict {
return &PutClustersConflict{}
}
// PutClustersConflict describes a response with status code 409, with default header values.
//
// The supplied LastModifiedDate is stale; reload the record before retrying.
type PutClustersConflict struct {
Payload *members_models.Error
}
// IsSuccess returns true when this put clusters conflict response has a 2xx status code
func (o *PutClustersConflict) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put clusters conflict response has a 3xx status code
func (o *PutClustersConflict) IsRedirect() bool {
return false
}
// IsClientError returns true when this put clusters conflict response has a 4xx status code
func (o *PutClustersConflict) IsClientError() bool {
return true
}
// IsServerError returns true when this put clusters conflict response has a 5xx status code
func (o *PutClustersConflict) IsServerError() bool {
return false
}
// IsCode returns true when this put clusters conflict response a status code equal to that given
func (o *PutClustersConflict) IsCode(code int) bool {
return code == 409
}
// Code gets the status code for the put clusters conflict response
func (o *PutClustersConflict) Code() int {
return 409
}
func (o *PutClustersConflict) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /clusters][%d] putClustersConflict %s", 409, payload)
}
func (o *PutClustersConflict) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /clusters][%d] putClustersConflict %s", 409, payload)
}
func (o *PutClustersConflict) GetPayload() *members_models.Error {
return o.Payload
}
func (o *PutClustersConflict) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(members_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutClustersUnprocessableEntity creates a PutClustersUnprocessableEntity with default headers values // NewPutClustersUnprocessableEntity creates a PutClustersUnprocessableEntity with default headers values
func NewPutClustersUnprocessableEntity() *PutClustersUnprocessableEntity { func NewPutClustersUnprocessableEntity() *PutClustersUnprocessableEntity {
return &PutClustersUnprocessableEntity{} return &PutClustersUnprocessableEntity{}

View File

@ -0,0 +1,67 @@
// (c) 2012-2020 by Taxnexus, Inc.
// All rights reserved worldwide.
// Proprietary product; unlicensed use is not allowed
package members_client
import (
"errors"
"fmt"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
)
const maxCredentialLength = 8192
// NewMembersCompoundAuth returns the auth writer required by governed Members
// operations: a service API key plus the active human kvSession cookie.
//
// Credential values are retained only inside the returned closure. Validation
// errors identify the invalid field without including its value.
func NewMembersCompoundAuth(apiKey, sessionToken string) (runtime.ClientAuthInfoWriter, error) {
if err := validateHeaderCredential("Members API key", apiKey); err != nil {
return nil, err
}
if err := validateCookieCredential("Members session token", sessionToken); err != nil {
return nil, err
}
return runtime.ClientAuthInfoWriterFunc(func(request runtime.ClientRequest, _ strfmt.Registry) error {
if err := request.SetHeaderParam("X-API-Key", apiKey); err != nil {
return fmt.Errorf("set Members API key header: %w", err)
}
if err := request.SetHeaderParam("Cookie", "kvSession="+sessionToken); err != nil {
return fmt.Errorf("set Members session cookie: %w", err)
}
return nil
}), nil
}
func validateHeaderCredential(name, value string) error {
if value == "" {
return fmt.Errorf("%s is required", name)
}
if len(value) > maxCredentialLength {
return fmt.Errorf("%s exceeds the maximum length", name)
}
for index := 0; index < len(value); index++ {
if value[index] < 0x21 || value[index] > 0x7e {
return fmt.Errorf("%s contains an invalid header byte", name)
}
}
return nil
}
func validateCookieCredential(name, value string) error {
if err := validateHeaderCredential(name, value); err != nil {
return err
}
for index := 0; index < len(value); index++ {
character := value[index]
if character == '"' || character == ',' || character == ';' || character == '\\' {
return errors.New(name + " contains an invalid cookie byte")
}
}
return nil
}

View File

@ -23,7 +23,7 @@ import (
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
) )
const membersProviderSpecSHA256 = "091a5ea132ea076ecfd391a02458e4a2445aa16dee89487b0aef977631766c4c" const membersProviderSpecSHA256 = "707dcee4b2078d431ba7f6c6a81105fb1c94c4b0b9298fa04567e63343af9a9d"
var learningTestAuth = openapiruntime.ClientAuthInfoWriterFunc(func(openapiruntime.ClientRequest, strfmt.Registry) error { var learningTestAuth = openapiruntime.ClientAuthInfoWriterFunc(func(openapiruntime.ClientRequest, strfmt.Registry) error {
return nil return nil
@ -517,7 +517,7 @@ func TestMembersSpecIsPinnedToProviderSource(t *testing.T) {
} }
sum := sha256.Sum256(mainSpec) sum := sha256.Sum256(mainSpec)
if got := hex.EncodeToString(sum[:]); got != membersProviderSpecSHA256 { if got := hex.EncodeToString(sum[:]); got != membersProviderSpecSHA256 {
t.Fatalf("Members spec drifted from integrated Track provider commit 6bfdcbe: SHA-256 = %s, want %s", got, membersProviderSpecSHA256) t.Fatalf("Members spec drifted from integrated Cluster provider commit a7cedc5: SHA-256 = %s, want %s", got, membersProviderSpecSHA256)
} }
externalSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "external", "members-vernonkeenan.yaml")) externalSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "external", "members-vernonkeenan.yaml"))

View File

@ -19,7 +19,7 @@ import (
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
) )
const membersTrackSpecSHA256 = "091a5ea132ea076ecfd391a02458e4a2445aa16dee89487b0aef977631766c4c" const membersTrackSpecSHA256 = "707dcee4b2078d431ba7f6c6a81105fb1c94c4b0b9298fa04567e63343af9a9d"
var trackTestAuth = openapiruntime.ClientAuthInfoWriterFunc( var trackTestAuth = openapiruntime.ClientAuthInfoWriterFunc(
func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil }, func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil },
@ -233,7 +233,7 @@ func TestMembersSpecIsPinnedToTrackProviderCommit(t *testing.T) {
sum := sha256.Sum256(mainSpec) sum := sha256.Sum256(mainSpec)
if got := hex.EncodeToString(sum[:]); got != membersTrackSpecSHA256 { if got := hex.EncodeToString(sum[:]); got != membersTrackSpecSHA256 {
t.Fatalf( t.Fatalf(
"Members spec drifted from integrated Track provider commit 6bfdcbe: SHA-256 = %s, want %s", "Members spec drifted from integrated Cluster provider commit a7cedc5: SHA-256 = %s, want %s",
got, membersTrackSpecSHA256, got, membersTrackSpecSHA256,
) )
} }

View File

@ -9,74 +9,439 @@ package members_models
import ( import (
"context" "context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/go-openapi/swag/jsonutils" "github.com/go-openapi/swag/jsonutils"
"github.com/go-openapi/swag/typeutils"
"github.com/go-openapi/validate"
) )
// Cluster cluster // Cluster Tenant-owned infrastructure metadata. TenantID, identity, and audit fields are server-owned.
// //
// swagger:model cluster // swagger:model Cluster
type Cluster struct { type Cluster struct {
// Created By // created by ID
// Read Only: true
// Max Length: 36
CreatedByID *string `json:"CreatedByID,omitempty"` CreatedByID *string `json:"CreatedByID,omitempty"`
// Created Date // created date
// Read Only: true
// Max Length: 64
CreatedDate *string `json:"CreatedDate,omitempty"` CreatedDate *string `json:"CreatedDate,omitempty"`
// Description // description
// Max Length: 255
Description *string `json:"Description,omitempty"` Description *string `json:"Description,omitempty"`
// Environment // environment
// Max Length: 255
Environment *string `json:"Environment,omitempty"` Environment *string `json:"Environment,omitempty"`
// Gateway // gateway
// Max Length: 255
Gateway *string `json:"Gateway,omitempty"` Gateway *string `json:"Gateway,omitempty"`
// Record Id // ID
// Max Length: 36
ID string `json:"ID,omitempty"` ID string `json:"ID,omitempty"`
// IP Address // IP address
// Max Length: 255
IPAddress *string `json:"IPAddress,omitempty"` IPAddress *string `json:"IPAddress,omitempty"`
// Last Modified By // last modified by ID
// Read Only: true
// Max Length: 36
LastModifiedByID *string `json:"LastModifiedByID,omitempty"` LastModifiedByID *string `json:"LastModifiedByID,omitempty"`
// Last Modified Date // last modified date
// Max Length: 64
LastModifiedDate *string `json:"LastModifiedDate,omitempty"` LastModifiedDate *string `json:"LastModifiedDate,omitempty"`
// Cluster Name // name
Name *string `json:"Name,omitempty"` // Required: true
// Max Length: 255
// Min Length: 1
Name *string `json:"Name"`
// Owner // owner ID
// Max Length: 36
OwnerID *string `json:"OwnerID,omitempty"` OwnerID *string `json:"OwnerID,omitempty"`
// External Reference // ref
// Max Length: 255
Ref *string `json:"Ref,omitempty"` Ref *string `json:"Ref,omitempty"`
// Status // status
// Max Length: 255
Status *string `json:"Status,omitempty"` Status *string `json:"Status,omitempty"`
// Subnet // subnet
// Max Length: 255
Subnet *string `json:"Subnet,omitempty"` Subnet *string `json:"Subnet,omitempty"`
// The ID of the tenant who owns this Database // tenant ID
// Read Only: true
// Max Length: 36
TenantID *string `json:"TenantID,omitempty"` TenantID *string `json:"TenantID,omitempty"`
// Type // type
// Max Length: 255
Type *string `json:"Type,omitempty"` Type *string `json:"Type,omitempty"`
// Zone // zone
// Max Length: 255
Zone *string `json:"Zone,omitempty"` Zone *string `json:"Zone,omitempty"`
} }
// Validate validates this cluster // Validate validates this cluster
func (m *Cluster) Validate(formats strfmt.Registry) error { func (m *Cluster) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCreatedByID(formats); err != nil {
res = append(res, err)
}
if err := m.validateCreatedDate(formats); err != nil {
res = append(res, err)
}
if err := m.validateDescription(formats); err != nil {
res = append(res, err)
}
if err := m.validateEnvironment(formats); err != nil {
res = append(res, err)
}
if err := m.validateGateway(formats); err != nil {
res = append(res, err)
}
if err := m.validateID(formats); err != nil {
res = append(res, err)
}
if err := m.validateIPAddress(formats); err != nil {
res = append(res, err)
}
if err := m.validateLastModifiedByID(formats); err != nil {
res = append(res, err)
}
if err := m.validateLastModifiedDate(formats); err != nil {
res = append(res, err)
}
if err := m.validateName(formats); err != nil {
res = append(res, err)
}
if err := m.validateOwnerID(formats); err != nil {
res = append(res, err)
}
if err := m.validateRef(formats); err != nil {
res = append(res, err)
}
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
if err := m.validateSubnet(formats); err != nil {
res = append(res, err)
}
if err := m.validateTenantID(formats); err != nil {
res = append(res, err)
}
if err := m.validateType(formats); err != nil {
res = append(res, err)
}
if err := m.validateZone(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil return nil
} }
// ContextValidate validates this cluster based on context it is used func (m *Cluster) validateCreatedByID(formats strfmt.Registry) error {
if typeutils.IsZero(m.CreatedByID) { // not required
return nil
}
if err := validate.MaxLength("CreatedByID", "body", *m.CreatedByID, 36); err != nil {
return err
}
return nil
}
func (m *Cluster) validateCreatedDate(formats strfmt.Registry) error {
if typeutils.IsZero(m.CreatedDate) { // not required
return nil
}
if err := validate.MaxLength("CreatedDate", "body", *m.CreatedDate, 64); err != nil {
return err
}
return nil
}
func (m *Cluster) validateDescription(formats strfmt.Registry) error {
if typeutils.IsZero(m.Description) { // not required
return nil
}
if err := validate.MaxLength("Description", "body", *m.Description, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateEnvironment(formats strfmt.Registry) error {
if typeutils.IsZero(m.Environment) { // not required
return nil
}
if err := validate.MaxLength("Environment", "body", *m.Environment, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateGateway(formats strfmt.Registry) error {
if typeutils.IsZero(m.Gateway) { // not required
return nil
}
if err := validate.MaxLength("Gateway", "body", *m.Gateway, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateID(formats strfmt.Registry) error {
if typeutils.IsZero(m.ID) { // not required
return nil
}
if err := validate.MaxLength("ID", "body", m.ID, 36); err != nil {
return err
}
return nil
}
func (m *Cluster) validateIPAddress(formats strfmt.Registry) error {
if typeutils.IsZero(m.IPAddress) { // not required
return nil
}
if err := validate.MaxLength("IPAddress", "body", *m.IPAddress, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateLastModifiedByID(formats strfmt.Registry) error {
if typeutils.IsZero(m.LastModifiedByID) { // not required
return nil
}
if err := validate.MaxLength("LastModifiedByID", "body", *m.LastModifiedByID, 36); err != nil {
return err
}
return nil
}
func (m *Cluster) validateLastModifiedDate(formats strfmt.Registry) error {
if typeutils.IsZero(m.LastModifiedDate) { // not required
return nil
}
if err := validate.MaxLength("LastModifiedDate", "body", *m.LastModifiedDate, 64); err != nil {
return err
}
return nil
}
func (m *Cluster) validateName(formats strfmt.Registry) error {
if err := validate.Required("Name", "body", m.Name); err != nil {
return err
}
if err := validate.MinLength("Name", "body", *m.Name, 1); err != nil {
return err
}
if err := validate.MaxLength("Name", "body", *m.Name, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateOwnerID(formats strfmt.Registry) error {
if typeutils.IsZero(m.OwnerID) { // not required
return nil
}
if err := validate.MaxLength("OwnerID", "body", *m.OwnerID, 36); err != nil {
return err
}
return nil
}
func (m *Cluster) validateRef(formats strfmt.Registry) error {
if typeutils.IsZero(m.Ref) { // not required
return nil
}
if err := validate.MaxLength("Ref", "body", *m.Ref, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateStatus(formats strfmt.Registry) error {
if typeutils.IsZero(m.Status) { // not required
return nil
}
if err := validate.MaxLength("Status", "body", *m.Status, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateSubnet(formats strfmt.Registry) error {
if typeutils.IsZero(m.Subnet) { // not required
return nil
}
if err := validate.MaxLength("Subnet", "body", *m.Subnet, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateTenantID(formats strfmt.Registry) error {
if typeutils.IsZero(m.TenantID) { // not required
return nil
}
if err := validate.MaxLength("TenantID", "body", *m.TenantID, 36); err != nil {
return err
}
return nil
}
func (m *Cluster) validateType(formats strfmt.Registry) error {
if typeutils.IsZero(m.Type) { // not required
return nil
}
if err := validate.MaxLength("Type", "body", *m.Type, 255); err != nil {
return err
}
return nil
}
func (m *Cluster) validateZone(formats strfmt.Registry) error {
if typeutils.IsZero(m.Zone) { // not required
return nil
}
if err := validate.MaxLength("Zone", "body", *m.Zone, 255); err != nil {
return err
}
return nil
}
// ContextValidate validate this cluster based on the context it is used
func (m *Cluster) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *Cluster) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateCreatedByID(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateCreatedDate(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateLastModifiedByID(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateTenantID(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *Cluster) contextValidateCreatedByID(ctx context.Context, formats strfmt.Registry) error {
if err := validate.ReadOnly(ctx, "CreatedByID", "body", m.CreatedByID); err != nil {
return err
}
return nil
}
func (m *Cluster) contextValidateCreatedDate(ctx context.Context, formats strfmt.Registry) error {
if err := validate.ReadOnly(ctx, "CreatedDate", "body", m.CreatedDate); err != nil {
return err
}
return nil
}
func (m *Cluster) contextValidateLastModifiedByID(ctx context.Context, formats strfmt.Registry) error {
if err := validate.ReadOnly(ctx, "LastModifiedByID", "body", m.LastModifiedByID); err != nil {
return err
}
return nil
}
func (m *Cluster) contextValidateTenantID(ctx context.Context, formats strfmt.Registry) error {
if err := validate.ReadOnly(ctx, "TenantID", "body", m.TenantID); err != nil {
return err
}
return nil return nil
} }

View File

@ -15,14 +15,18 @@ import (
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/go-openapi/swag/jsonutils" "github.com/go-openapi/swag/jsonutils"
"github.com/go-openapi/swag/typeutils" "github.com/go-openapi/swag/typeutils"
"github.com/go-openapi/validate"
) )
// ClusterRequest cluster request // ClusterRequest Exactly one governed Cluster object
// //
// swagger:model ClusterRequest // swagger:model ClusterRequest
type ClusterRequest struct { type ClusterRequest struct {
// data // data
// Required: true
// Max Items: 1
// Min Items: 1
Data []*Cluster `json:"Data"` Data []*Cluster `json:"Data"`
} }
@ -41,8 +45,19 @@ func (m *ClusterRequest) Validate(formats strfmt.Registry) error {
} }
func (m *ClusterRequest) validateData(formats strfmt.Registry) error { func (m *ClusterRequest) validateData(formats strfmt.Registry) error {
if typeutils.IsZero(m.Data) { // not required
return nil if err := validate.Required("Data", "body", m.Data); err != nil {
return err
}
iDataSize := int64(len(m.Data))
if err := validate.MinItems("Data", "body", iDataSize, 1); err != nil {
return err
}
if err := validate.MaxItems("Data", "body", iDataSize, 1); err != nil {
return err
} }
for i := 0; i < len(m.Data); i++ { for i := 0; i < len(m.Data); i++ {

View File

@ -17,7 +17,7 @@ import (
"github.com/go-openapi/swag/typeutils" "github.com/go-openapi/swag/typeutils"
) )
// ClusterResponse An array of cluster objects // ClusterResponse An array of governed tenant Cluster objects
// //
// swagger:model ClusterResponse // swagger:model ClusterResponse
type ClusterResponse struct { type ClusterResponse struct {

View File

@ -154,8 +154,16 @@ parameters:
name: clusterId name: clusterId
required: false required: false
type: string type: string
clusterTenantId:
description: Tenant that owns the Cluster. Members verifies the active human membership and never accepts TenantID from the body.
in: query
name: tenantId
required: true
type: string
minLength: 1
maxLength: 36
ClusterRequest: ClusterRequest:
description: An array of Cluster records description: Exactly one governed Cluster record
in: body in: body
name: ClusterRequest name: ClusterRequest
required: true required: true
@ -1807,10 +1815,11 @@ paths:
- Attendees - Attendees
/clusters: /clusters:
get: get:
description: Return a list of Cluster records from the datastore description: Return tenant-scoped Cluster records. Requires members:cluster:read and an active human membership in tenantId.
operationId: getClusters operationId: getClusters
parameters: parameters:
- $ref: "#/parameters/clusterIdQuery" - $ref: "#/parameters/clusterIdQuery"
- $ref: "#/parameters/clusterTenantId"
- $ref: "#/parameters/limitQuery" - $ref: "#/parameters/limitQuery"
- $ref: "#/parameters/offsetQuery" - $ref: "#/parameters/offsetQuery"
responses: responses:
@ -1828,14 +1837,16 @@ paths:
$ref: "#/responses/ServerError" $ref: "#/responses/ServerError"
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get a list Clusters kvSessionCookie: []
summary: Get governed tenant Clusters
tags: tags:
- Clusters - Clusters
post: post:
description: Create Clusters description: Create one tenant-scoped Cluster. Requires members:cluster:create and active Owner or Manager access. ID, TenantID, and audit fields are server-owned.
operationId: postClusters operationId: postClusters
parameters: parameters:
- $ref: "#/parameters/ClusterRequest" - $ref: "#/parameters/ClusterRequest"
- $ref: "#/parameters/clusterTenantId"
responses: responses:
"200": "200":
$ref: "#/responses/ClusterResponse" $ref: "#/responses/ClusterResponse"
@ -1851,14 +1862,16 @@ paths:
$ref: "#/responses/ServerError" $ref: "#/responses/ServerError"
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Create new Clusters kvSessionCookie: []
summary: Create a governed Cluster
tags: tags:
- Clusters - Clusters
put: put:
description: Update Cluster description: CAS-update one tenant-scoped Cluster. Requires members:cluster:update, active Owner or Manager access, and the current LastModifiedDate. TenantID and audit fields remain server-owned.
operationId: putClusters operationId: putClusters
parameters: parameters:
- $ref: "#/parameters/ClusterRequest" - $ref: "#/parameters/ClusterRequest"
- $ref: "#/parameters/clusterTenantId"
responses: responses:
"200": "200":
$ref: "#/responses/ClusterResponse" $ref: "#/responses/ClusterResponse"
@ -1866,6 +1879,8 @@ paths:
$ref: "#/responses/Unauthorized" $ref: "#/responses/Unauthorized"
"403": "403":
$ref: "#/responses/AccessForbidden" $ref: "#/responses/AccessForbidden"
"409":
$ref: "#/responses/Conflict"
"404": "404":
$ref: "#/responses/NotFound" $ref: "#/responses/NotFound"
"422": "422":
@ -1874,7 +1889,8 @@ paths:
$ref: "#/responses/ServerError" $ref: "#/responses/ServerError"
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Update Clustera kvSessionCookie: []
summary: Update a governed Cluster
tags: tags:
- Clusters - Clusters
/courselessons: /courselessons:
@ -4363,6 +4379,83 @@ paths:
tags: tags:
- Users - Users
definitions: definitions:
Cluster:
description: Tenant-owned infrastructure metadata. TenantID, identity, and audit fields are server-owned.
type: object
required: [Name]
properties:
ID:
type: string
maxLength: 36
CreatedByID:
type: string
maxLength: 36
x-nullable: true
readOnly: true
CreatedDate:
type: string
maxLength: 64
x-nullable: true
readOnly: true
Description:
type: string
maxLength: 255
x-nullable: true
Environment:
type: string
maxLength: 255
x-nullable: true
Gateway:
type: string
maxLength: 255
x-nullable: true
IPAddress:
type: string
maxLength: 255
x-nullable: true
LastModifiedByID:
type: string
maxLength: 36
x-nullable: true
readOnly: true
LastModifiedDate:
type: string
maxLength: 64
x-nullable: true
Name:
type: string
minLength: 1
maxLength: 255
x-nullable: true
OwnerID:
type: string
maxLength: 36
x-nullable: true
Ref:
type: string
maxLength: 255
x-nullable: true
Status:
type: string
maxLength: 255
x-nullable: true
Subnet:
type: string
maxLength: 255
x-nullable: true
TenantID:
type: string
maxLength: 36
x-nullable: true
readOnly: true
Type:
type: string
maxLength: 255
x-nullable: true
Zone:
type: string
maxLength: 255
x-nullable: true
Track: Track:
description: A governed Track root. The schema has no tenant key, so Members derives ownership from the server-owned creator and active Keenan Vision tenant membership. description: A governed Track root. The schema has no tenant key, so Members derives ownership from the server-owned creator and active Keenan Vision tenant membership.
type: object type: object
@ -4793,18 +4886,22 @@ definitions:
type: array type: array
type: object type: object
ClusterRequest: ClusterRequest:
description: Exactly one governed Cluster object
required: [Data]
properties: properties:
Data: Data:
items: items:
$ref: "../../lib/swagger/defs/cluster.yaml#/Cluster" $ref: "#/definitions/Cluster"
minItems: 1
maxItems: 1
type: array type: array
type: object type: object
ClusterResponse: ClusterResponse:
description: An array of cluster objects description: An array of governed tenant Cluster objects
properties: properties:
Data: Data:
items: items:
$ref: "../../lib/swagger/defs/cluster.yaml#/Cluster" $ref: "#/definitions/Cluster"
type: array type: array
Meta: Meta:
$ref: "#/definitions/ResponseMeta" $ref: "#/definitions/ResponseMeta"

View File

@ -154,8 +154,16 @@ parameters:
name: clusterId name: clusterId
required: false required: false
type: string type: string
clusterTenantId:
description: Tenant that owns the Cluster. Members verifies the active human membership and never accepts TenantID from the body.
in: query
name: tenantId
required: true
type: string
minLength: 1
maxLength: 36
ClusterRequest: ClusterRequest:
description: An array of Cluster records description: Exactly one governed Cluster record
in: body in: body
name: ClusterRequest name: ClusterRequest
required: true required: true
@ -1807,10 +1815,11 @@ paths:
- Attendees - Attendees
/clusters: /clusters:
get: get:
description: Return a list of Cluster records from the datastore description: Return tenant-scoped Cluster records. Requires members:cluster:read and an active human membership in tenantId.
operationId: getClusters operationId: getClusters
parameters: parameters:
- $ref: "#/parameters/clusterIdQuery" - $ref: "#/parameters/clusterIdQuery"
- $ref: "#/parameters/clusterTenantId"
- $ref: "#/parameters/limitQuery" - $ref: "#/parameters/limitQuery"
- $ref: "#/parameters/offsetQuery" - $ref: "#/parameters/offsetQuery"
responses: responses:
@ -1828,14 +1837,16 @@ paths:
$ref: "#/responses/ServerError" $ref: "#/responses/ServerError"
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get a list Clusters kvSessionCookie: []
summary: Get governed tenant Clusters
tags: tags:
- Clusters - Clusters
post: post:
description: Create Clusters description: Create one tenant-scoped Cluster. Requires members:cluster:create and active Owner or Manager access. ID, TenantID, and audit fields are server-owned.
operationId: postClusters operationId: postClusters
parameters: parameters:
- $ref: "#/parameters/ClusterRequest" - $ref: "#/parameters/ClusterRequest"
- $ref: "#/parameters/clusterTenantId"
responses: responses:
"200": "200":
$ref: "#/responses/ClusterResponse" $ref: "#/responses/ClusterResponse"
@ -1851,14 +1862,16 @@ paths:
$ref: "#/responses/ServerError" $ref: "#/responses/ServerError"
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Create new Clusters kvSessionCookie: []
summary: Create a governed Cluster
tags: tags:
- Clusters - Clusters
put: put:
description: Update Cluster description: CAS-update one tenant-scoped Cluster. Requires members:cluster:update, active Owner or Manager access, and the current LastModifiedDate. TenantID and audit fields remain server-owned.
operationId: putClusters operationId: putClusters
parameters: parameters:
- $ref: "#/parameters/ClusterRequest" - $ref: "#/parameters/ClusterRequest"
- $ref: "#/parameters/clusterTenantId"
responses: responses:
"200": "200":
$ref: "#/responses/ClusterResponse" $ref: "#/responses/ClusterResponse"
@ -1866,6 +1879,8 @@ paths:
$ref: "#/responses/Unauthorized" $ref: "#/responses/Unauthorized"
"403": "403":
$ref: "#/responses/AccessForbidden" $ref: "#/responses/AccessForbidden"
"409":
$ref: "#/responses/Conflict"
"404": "404":
$ref: "#/responses/NotFound" $ref: "#/responses/NotFound"
"422": "422":
@ -1874,7 +1889,8 @@ paths:
$ref: "#/responses/ServerError" $ref: "#/responses/ServerError"
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Update Clustera kvSessionCookie: []
summary: Update a governed Cluster
tags: tags:
- Clusters - Clusters
/courselessons: /courselessons:
@ -4363,6 +4379,83 @@ paths:
tags: tags:
- Users - Users
definitions: definitions:
Cluster:
description: Tenant-owned infrastructure metadata. TenantID, identity, and audit fields are server-owned.
type: object
required: [Name]
properties:
ID:
type: string
maxLength: 36
CreatedByID:
type: string
maxLength: 36
x-nullable: true
readOnly: true
CreatedDate:
type: string
maxLength: 64
x-nullable: true
readOnly: true
Description:
type: string
maxLength: 255
x-nullable: true
Environment:
type: string
maxLength: 255
x-nullable: true
Gateway:
type: string
maxLength: 255
x-nullable: true
IPAddress:
type: string
maxLength: 255
x-nullable: true
LastModifiedByID:
type: string
maxLength: 36
x-nullable: true
readOnly: true
LastModifiedDate:
type: string
maxLength: 64
x-nullable: true
Name:
type: string
minLength: 1
maxLength: 255
x-nullable: true
OwnerID:
type: string
maxLength: 36
x-nullable: true
Ref:
type: string
maxLength: 255
x-nullable: true
Status:
type: string
maxLength: 255
x-nullable: true
Subnet:
type: string
maxLength: 255
x-nullable: true
TenantID:
type: string
maxLength: 36
x-nullable: true
readOnly: true
Type:
type: string
maxLength: 255
x-nullable: true
Zone:
type: string
maxLength: 255
x-nullable: true
Track: Track:
description: A governed Track root. The schema has no tenant key, so Members derives ownership from the server-owned creator and active Keenan Vision tenant membership. description: A governed Track root. The schema has no tenant key, so Members derives ownership from the server-owned creator and active Keenan Vision tenant membership.
type: object type: object
@ -4793,18 +4886,22 @@ definitions:
type: array type: array
type: object type: object
ClusterRequest: ClusterRequest:
description: Exactly one governed Cluster object
required: [Data]
properties: properties:
Data: Data:
items: items:
$ref: "../../lib/swagger/defs/cluster.yaml#/Cluster" $ref: "#/definitions/Cluster"
minItems: 1
maxItems: 1
type: array type: array
type: object type: object
ClusterResponse: ClusterResponse:
description: An array of cluster objects description: An array of governed tenant Cluster objects
properties: properties:
Data: Data:
items: items:
$ref: "../../lib/swagger/defs/cluster.yaml#/Cluster" $ref: "#/definitions/Cluster"
type: array type: array
Meta: Meta:
$ref: "#/definitions/ResponseMeta" $ref: "#/definitions/ResponseMeta"