diff --git a/internal/build/build.go b/internal/build/build.go index 4cd3897..5c4351c 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -22,7 +22,8 @@ var ( //goland:noinspection GoBoolExpressions IsDev = Version == "0.0.0-dev" && !forceProd - PlatformBaseURI = "https://container-platform-backend.apps.intilitycloud.com" + PlatformBaseURI = "https://container-platform-backend.apps.intilitycloud.com" + PlatformBaseURIBlurite = "https://container-platform-blurite.apps.intilitycloud.com" // SentryDSN is injected in the build from the CI/CD pipeline. // It is disabled by default. @@ -119,6 +120,14 @@ func PlatformAPIHost() string { return PlatformBaseURI } +func PlatformAPIHostBlurite() string { + if IsDev { + return "http://localhost:8082" + } + + return PlatformBaseURIBlurite +} + func ClientID() string { if IsDev { return "27f5ab79-28cb-4824-b603-4b0795b8985e" diff --git a/mocks/AIAPIKeyClient.go b/mocks/AIAPIKeyClient.go new file mode 100644 index 0000000..7f3f543 --- /dev/null +++ b/mocks/AIAPIKeyClient.go @@ -0,0 +1,265 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "github.com/intility/indev/pkg/client" + + mock "github.com/stretchr/testify/mock" +) + +// AIAPIKeyClient is an autogenerated mock type for the AIAPIKeyClient type +type AIAPIKeyClient struct { + mock.Mock +} + +type AIAPIKeyClient_Expecter struct { + mock *mock.Mock +} + +func (_m *AIAPIKeyClient) EXPECT() *AIAPIKeyClient_Expecter { + return &AIAPIKeyClient_Expecter{mock: &_m.Mock} +} + +// CreateAIAPIKey provides a mock function with given fields: ctx, deploymentID, request +func (_m *AIAPIKeyClient) CreateAIAPIKey(ctx context.Context, deploymentID string, request client.NewAIAPIKeyRequest) (*client.AIAPIKeyWithSecret, error) { + ret := _m.Called(ctx, deploymentID, request) + + if len(ret) == 0 { + panic("no return value specified for CreateAIAPIKey") + } + + var r0 *client.AIAPIKeyWithSecret + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.NewAIAPIKeyRequest) (*client.AIAPIKeyWithSecret, error)); ok { + return rf(ctx, deploymentID, request) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.NewAIAPIKeyRequest) *client.AIAPIKeyWithSecret); ok { + r0 = rf(ctx, deploymentID, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIAPIKeyWithSecret) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.NewAIAPIKeyRequest) error); ok { + r1 = rf(ctx, deploymentID, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIAPIKeyClient_CreateAIAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAIAPIKey' +type AIAPIKeyClient_CreateAIAPIKey_Call struct { + *mock.Call +} + +// CreateAIAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +// - request client.NewAIAPIKeyRequest +func (_e *AIAPIKeyClient_Expecter) CreateAIAPIKey(ctx interface{}, deploymentID interface{}, request interface{}) *AIAPIKeyClient_CreateAIAPIKey_Call { + return &AIAPIKeyClient_CreateAIAPIKey_Call{Call: _e.mock.On("CreateAIAPIKey", ctx, deploymentID, request)} +} + +func (_c *AIAPIKeyClient_CreateAIAPIKey_Call) Run(run func(ctx context.Context, deploymentID string, request client.NewAIAPIKeyRequest)) *AIAPIKeyClient_CreateAIAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(client.NewAIAPIKeyRequest)) + }) + return _c +} + +func (_c *AIAPIKeyClient_CreateAIAPIKey_Call) Return(_a0 *client.AIAPIKeyWithSecret, _a1 error) *AIAPIKeyClient_CreateAIAPIKey_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIAPIKeyClient_CreateAIAPIKey_Call) RunAndReturn(run func(context.Context, string, client.NewAIAPIKeyRequest) (*client.AIAPIKeyWithSecret, error)) *AIAPIKeyClient_CreateAIAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// DeleteAIAPIKey provides a mock function with given fields: ctx, deploymentID, keyID +func (_m *AIAPIKeyClient) DeleteAIAPIKey(ctx context.Context, deploymentID string, keyID string) error { + ret := _m.Called(ctx, deploymentID, keyID) + + if len(ret) == 0 { + panic("no return value specified for DeleteAIAPIKey") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, deploymentID, keyID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AIAPIKeyClient_DeleteAIAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAIAPIKey' +type AIAPIKeyClient_DeleteAIAPIKey_Call struct { + *mock.Call +} + +// DeleteAIAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +// - keyID string +func (_e *AIAPIKeyClient_Expecter) DeleteAIAPIKey(ctx interface{}, deploymentID interface{}, keyID interface{}) *AIAPIKeyClient_DeleteAIAPIKey_Call { + return &AIAPIKeyClient_DeleteAIAPIKey_Call{Call: _e.mock.On("DeleteAIAPIKey", ctx, deploymentID, keyID)} +} + +func (_c *AIAPIKeyClient_DeleteAIAPIKey_Call) Run(run func(ctx context.Context, deploymentID string, keyID string)) *AIAPIKeyClient_DeleteAIAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *AIAPIKeyClient_DeleteAIAPIKey_Call) Return(_a0 error) *AIAPIKeyClient_DeleteAIAPIKey_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AIAPIKeyClient_DeleteAIAPIKey_Call) RunAndReturn(run func(context.Context, string, string) error) *AIAPIKeyClient_DeleteAIAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// GetAIAPIKey provides a mock function with given fields: ctx, deploymentID, name +func (_m *AIAPIKeyClient) GetAIAPIKey(ctx context.Context, deploymentID string, name string) (*client.AIAPIKey, error) { + ret := _m.Called(ctx, deploymentID, name) + + if len(ret) == 0 { + panic("no return value specified for GetAIAPIKey") + } + + var r0 *client.AIAPIKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*client.AIAPIKey, error)); ok { + return rf(ctx, deploymentID, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *client.AIAPIKey); ok { + r0 = rf(ctx, deploymentID, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIAPIKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, deploymentID, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIAPIKeyClient_GetAIAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAIAPIKey' +type AIAPIKeyClient_GetAIAPIKey_Call struct { + *mock.Call +} + +// GetAIAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +// - name string +func (_e *AIAPIKeyClient_Expecter) GetAIAPIKey(ctx interface{}, deploymentID interface{}, name interface{}) *AIAPIKeyClient_GetAIAPIKey_Call { + return &AIAPIKeyClient_GetAIAPIKey_Call{Call: _e.mock.On("GetAIAPIKey", ctx, deploymentID, name)} +} + +func (_c *AIAPIKeyClient_GetAIAPIKey_Call) Run(run func(ctx context.Context, deploymentID string, name string)) *AIAPIKeyClient_GetAIAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *AIAPIKeyClient_GetAIAPIKey_Call) Return(_a0 *client.AIAPIKey, _a1 error) *AIAPIKeyClient_GetAIAPIKey_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIAPIKeyClient_GetAIAPIKey_Call) RunAndReturn(run func(context.Context, string, string) (*client.AIAPIKey, error)) *AIAPIKeyClient_GetAIAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// ListAIAPIKeys provides a mock function with given fields: ctx, deploymentID +func (_m *AIAPIKeyClient) ListAIAPIKeys(ctx context.Context, deploymentID string) ([]client.AIAPIKey, error) { + ret := _m.Called(ctx, deploymentID) + + if len(ret) == 0 { + panic("no return value specified for ListAIAPIKeys") + } + + var r0 []client.AIAPIKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]client.AIAPIKey, error)); ok { + return rf(ctx, deploymentID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []client.AIAPIKey); ok { + r0 = rf(ctx, deploymentID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.AIAPIKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, deploymentID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIAPIKeyClient_ListAIAPIKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAIAPIKeys' +type AIAPIKeyClient_ListAIAPIKeys_Call struct { + *mock.Call +} + +// ListAIAPIKeys is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +func (_e *AIAPIKeyClient_Expecter) ListAIAPIKeys(ctx interface{}, deploymentID interface{}) *AIAPIKeyClient_ListAIAPIKeys_Call { + return &AIAPIKeyClient_ListAIAPIKeys_Call{Call: _e.mock.On("ListAIAPIKeys", ctx, deploymentID)} +} + +func (_c *AIAPIKeyClient_ListAIAPIKeys_Call) Run(run func(ctx context.Context, deploymentID string)) *AIAPIKeyClient_ListAIAPIKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *AIAPIKeyClient_ListAIAPIKeys_Call) Return(_a0 []client.AIAPIKey, _a1 error) *AIAPIKeyClient_ListAIAPIKeys_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIAPIKeyClient_ListAIAPIKeys_Call) RunAndReturn(run func(context.Context, string) ([]client.AIAPIKey, error)) *AIAPIKeyClient_ListAIAPIKeys_Call { + _c.Call.Return(run) + return _c +} + +// NewAIAPIKeyClient creates a new instance of AIAPIKeyClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAIAPIKeyClient(t interface { + mock.TestingT + Cleanup(func()) +}) *AIAPIKeyClient { + mock := &AIAPIKeyClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/AIClient.go b/mocks/AIClient.go new file mode 100644 index 0000000..6957a9e --- /dev/null +++ b/mocks/AIClient.go @@ -0,0 +1,319 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "github.com/intility/indev/pkg/client" + + mock "github.com/stretchr/testify/mock" +) + +// AIClient is an autogenerated mock type for the AIClient type +type AIClient struct { + mock.Mock +} + +type AIClient_Expecter struct { + mock *mock.Mock +} + +func (_m *AIClient) EXPECT() *AIClient_Expecter { + return &AIClient_Expecter{mock: &_m.Mock} +} + +// CreateAIDeployment provides a mock function with given fields: ctx, request +func (_m *AIClient) CreateAIDeployment(ctx context.Context, request client.NewAIDeploymentRequest) (*client.AIDeployment, error) { + ret := _m.Called(ctx, request) + + if len(ret) == 0 { + panic("no return value specified for CreateAIDeployment") + } + + var r0 *client.AIDeployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, client.NewAIDeploymentRequest) (*client.AIDeployment, error)); ok { + return rf(ctx, request) + } + if rf, ok := ret.Get(0).(func(context.Context, client.NewAIDeploymentRequest) *client.AIDeployment); ok { + r0 = rf(ctx, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIDeployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, client.NewAIDeploymentRequest) error); ok { + r1 = rf(ctx, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIClient_CreateAIDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAIDeployment' +type AIClient_CreateAIDeployment_Call struct { + *mock.Call +} + +// CreateAIDeployment is a helper method to define mock.On call +// - ctx context.Context +// - request client.NewAIDeploymentRequest +func (_e *AIClient_Expecter) CreateAIDeployment(ctx interface{}, request interface{}) *AIClient_CreateAIDeployment_Call { + return &AIClient_CreateAIDeployment_Call{Call: _e.mock.On("CreateAIDeployment", ctx, request)} +} + +func (_c *AIClient_CreateAIDeployment_Call) Run(run func(ctx context.Context, request client.NewAIDeploymentRequest)) *AIClient_CreateAIDeployment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(client.NewAIDeploymentRequest)) + }) + return _c +} + +func (_c *AIClient_CreateAIDeployment_Call) Return(_a0 *client.AIDeployment, _a1 error) *AIClient_CreateAIDeployment_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIClient_CreateAIDeployment_Call) RunAndReturn(run func(context.Context, client.NewAIDeploymentRequest) (*client.AIDeployment, error)) *AIClient_CreateAIDeployment_Call { + _c.Call.Return(run) + return _c +} + +// DeleteAIDeployment provides a mock function with given fields: ctx, id +func (_m *AIClient) DeleteAIDeployment(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteAIDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AIClient_DeleteAIDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAIDeployment' +type AIClient_DeleteAIDeployment_Call struct { + *mock.Call +} + +// DeleteAIDeployment is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *AIClient_Expecter) DeleteAIDeployment(ctx interface{}, id interface{}) *AIClient_DeleteAIDeployment_Call { + return &AIClient_DeleteAIDeployment_Call{Call: _e.mock.On("DeleteAIDeployment", ctx, id)} +} + +func (_c *AIClient_DeleteAIDeployment_Call) Run(run func(ctx context.Context, id string)) *AIClient_DeleteAIDeployment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *AIClient_DeleteAIDeployment_Call) Return(_a0 error) *AIClient_DeleteAIDeployment_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AIClient_DeleteAIDeployment_Call) RunAndReturn(run func(context.Context, string) error) *AIClient_DeleteAIDeployment_Call { + _c.Call.Return(run) + return _c +} + +// GetAIDeployment provides a mock function with given fields: ctx, name +func (_m *AIClient) GetAIDeployment(ctx context.Context, name string) (*client.AIDeployment, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetAIDeployment") + } + + var r0 *client.AIDeployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*client.AIDeployment, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *client.AIDeployment); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIDeployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIClient_GetAIDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAIDeployment' +type AIClient_GetAIDeployment_Call struct { + *mock.Call +} + +// GetAIDeployment is a helper method to define mock.On call +// - ctx context.Context +// - name string +func (_e *AIClient_Expecter) GetAIDeployment(ctx interface{}, name interface{}) *AIClient_GetAIDeployment_Call { + return &AIClient_GetAIDeployment_Call{Call: _e.mock.On("GetAIDeployment", ctx, name)} +} + +func (_c *AIClient_GetAIDeployment_Call) Run(run func(ctx context.Context, name string)) *AIClient_GetAIDeployment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *AIClient_GetAIDeployment_Call) Return(_a0 *client.AIDeployment, _a1 error) *AIClient_GetAIDeployment_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIClient_GetAIDeployment_Call) RunAndReturn(run func(context.Context, string) (*client.AIDeployment, error)) *AIClient_GetAIDeployment_Call { + _c.Call.Return(run) + return _c +} + +// ListAIDeployments provides a mock function with given fields: ctx +func (_m *AIClient) ListAIDeployments(ctx context.Context) ([]client.AIDeployment, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListAIDeployments") + } + + var r0 []client.AIDeployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]client.AIDeployment, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []client.AIDeployment); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.AIDeployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIClient_ListAIDeployments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAIDeployments' +type AIClient_ListAIDeployments_Call struct { + *mock.Call +} + +// ListAIDeployments is a helper method to define mock.On call +// - ctx context.Context +func (_e *AIClient_Expecter) ListAIDeployments(ctx interface{}) *AIClient_ListAIDeployments_Call { + return &AIClient_ListAIDeployments_Call{Call: _e.mock.On("ListAIDeployments", ctx)} +} + +func (_c *AIClient_ListAIDeployments_Call) Run(run func(ctx context.Context)) *AIClient_ListAIDeployments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *AIClient_ListAIDeployments_Call) Return(_a0 []client.AIDeployment, _a1 error) *AIClient_ListAIDeployments_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIClient_ListAIDeployments_Call) RunAndReturn(run func(context.Context) ([]client.AIDeployment, error)) *AIClient_ListAIDeployments_Call { + _c.Call.Return(run) + return _c +} + +// ListAIModels provides a mock function with given fields: ctx +func (_m *AIClient) ListAIModels(ctx context.Context) ([]client.AIModel, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListAIModels") + } + + var r0 []client.AIModel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]client.AIModel, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []client.AIModel); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.AIModel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AIClient_ListAIModels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAIModels' +type AIClient_ListAIModels_Call struct { + *mock.Call +} + +// ListAIModels is a helper method to define mock.On call +// - ctx context.Context +func (_e *AIClient_Expecter) ListAIModels(ctx interface{}) *AIClient_ListAIModels_Call { + return &AIClient_ListAIModels_Call{Call: _e.mock.On("ListAIModels", ctx)} +} + +func (_c *AIClient_ListAIModels_Call) Run(run func(ctx context.Context)) *AIClient_ListAIModels_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *AIClient_ListAIModels_Call) Return(_a0 []client.AIModel, _a1 error) *AIClient_ListAIModels_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AIClient_ListAIModels_Call) RunAndReturn(run func(context.Context) ([]client.AIModel, error)) *AIClient_ListAIModels_Call { + _c.Call.Return(run) + return _c +} + +// NewAIClient creates a new instance of AIClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAIClient(t interface { + mock.TestingT + Cleanup(func()) +}) *AIClient { + mock := &AIClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/Client.go b/mocks/Client.go index e4dc286..7a3effd 100644 --- a/mocks/Client.go +++ b/mocks/Client.go @@ -119,6 +119,125 @@ func (_c *Client_AddTeamMember_Call) RunAndReturn(run func(context.Context, stri return _c } +// CreateAIAPIKey provides a mock function with given fields: ctx, deploymentID, request +func (_m *Client) CreateAIAPIKey(ctx context.Context, deploymentID string, request client.NewAIAPIKeyRequest) (*client.AIAPIKeyWithSecret, error) { + ret := _m.Called(ctx, deploymentID, request) + + if len(ret) == 0 { + panic("no return value specified for CreateAIAPIKey") + } + + var r0 *client.AIAPIKeyWithSecret + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, client.NewAIAPIKeyRequest) (*client.AIAPIKeyWithSecret, error)); ok { + return rf(ctx, deploymentID, request) + } + if rf, ok := ret.Get(0).(func(context.Context, string, client.NewAIAPIKeyRequest) *client.AIAPIKeyWithSecret); ok { + r0 = rf(ctx, deploymentID, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIAPIKeyWithSecret) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, client.NewAIAPIKeyRequest) error); ok { + r1 = rf(ctx, deploymentID, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_CreateAIAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAIAPIKey' +type Client_CreateAIAPIKey_Call struct { + *mock.Call +} + +// CreateAIAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +// - request client.NewAIAPIKeyRequest +func (_e *Client_Expecter) CreateAIAPIKey(ctx interface{}, deploymentID interface{}, request interface{}) *Client_CreateAIAPIKey_Call { + return &Client_CreateAIAPIKey_Call{Call: _e.mock.On("CreateAIAPIKey", ctx, deploymentID, request)} +} + +func (_c *Client_CreateAIAPIKey_Call) Run(run func(ctx context.Context, deploymentID string, request client.NewAIAPIKeyRequest)) *Client_CreateAIAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(client.NewAIAPIKeyRequest)) + }) + return _c +} + +func (_c *Client_CreateAIAPIKey_Call) Return(_a0 *client.AIAPIKeyWithSecret, _a1 error) *Client_CreateAIAPIKey_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_CreateAIAPIKey_Call) RunAndReturn(run func(context.Context, string, client.NewAIAPIKeyRequest) (*client.AIAPIKeyWithSecret, error)) *Client_CreateAIAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// CreateAIDeployment provides a mock function with given fields: ctx, request +func (_m *Client) CreateAIDeployment(ctx context.Context, request client.NewAIDeploymentRequest) (*client.AIDeployment, error) { + ret := _m.Called(ctx, request) + + if len(ret) == 0 { + panic("no return value specified for CreateAIDeployment") + } + + var r0 *client.AIDeployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, client.NewAIDeploymentRequest) (*client.AIDeployment, error)); ok { + return rf(ctx, request) + } + if rf, ok := ret.Get(0).(func(context.Context, client.NewAIDeploymentRequest) *client.AIDeployment); ok { + r0 = rf(ctx, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIDeployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, client.NewAIDeploymentRequest) error); ok { + r1 = rf(ctx, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_CreateAIDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAIDeployment' +type Client_CreateAIDeployment_Call struct { + *mock.Call +} + +// CreateAIDeployment is a helper method to define mock.On call +// - ctx context.Context +// - request client.NewAIDeploymentRequest +func (_e *Client_Expecter) CreateAIDeployment(ctx interface{}, request interface{}) *Client_CreateAIDeployment_Call { + return &Client_CreateAIDeployment_Call{Call: _e.mock.On("CreateAIDeployment", ctx, request)} +} + +func (_c *Client_CreateAIDeployment_Call) Run(run func(ctx context.Context, request client.NewAIDeploymentRequest)) *Client_CreateAIDeployment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(client.NewAIDeploymentRequest)) + }) + return _c +} + +func (_c *Client_CreateAIDeployment_Call) Return(_a0 *client.AIDeployment, _a1 error) *Client_CreateAIDeployment_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_CreateAIDeployment_Call) RunAndReturn(run func(context.Context, client.NewAIDeploymentRequest) (*client.AIDeployment, error)) *Client_CreateAIDeployment_Call { + _c.Call.Return(run) + return _c +} + // CreateCluster provides a mock function with given fields: ctx, request func (_m *Client) CreateCluster(ctx context.Context, request client.NewClusterRequest) (*client.Cluster, error) { ret := _m.Called(ctx, request) @@ -237,6 +356,101 @@ func (_c *Client_CreateTeam_Call) RunAndReturn(run func(context.Context, client. return _c } +// DeleteAIAPIKey provides a mock function with given fields: ctx, deploymentID, keyID +func (_m *Client) DeleteAIAPIKey(ctx context.Context, deploymentID string, keyID string) error { + ret := _m.Called(ctx, deploymentID, keyID) + + if len(ret) == 0 { + panic("no return value specified for DeleteAIAPIKey") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, deploymentID, keyID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Client_DeleteAIAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAIAPIKey' +type Client_DeleteAIAPIKey_Call struct { + *mock.Call +} + +// DeleteAIAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +// - keyID string +func (_e *Client_Expecter) DeleteAIAPIKey(ctx interface{}, deploymentID interface{}, keyID interface{}) *Client_DeleteAIAPIKey_Call { + return &Client_DeleteAIAPIKey_Call{Call: _e.mock.On("DeleteAIAPIKey", ctx, deploymentID, keyID)} +} + +func (_c *Client_DeleteAIAPIKey_Call) Run(run func(ctx context.Context, deploymentID string, keyID string)) *Client_DeleteAIAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *Client_DeleteAIAPIKey_Call) Return(_a0 error) *Client_DeleteAIAPIKey_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Client_DeleteAIAPIKey_Call) RunAndReturn(run func(context.Context, string, string) error) *Client_DeleteAIAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// DeleteAIDeployment provides a mock function with given fields: ctx, id +func (_m *Client) DeleteAIDeployment(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteAIDeployment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Client_DeleteAIDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAIDeployment' +type Client_DeleteAIDeployment_Call struct { + *mock.Call +} + +// DeleteAIDeployment is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *Client_Expecter) DeleteAIDeployment(ctx interface{}, id interface{}) *Client_DeleteAIDeployment_Call { + return &Client_DeleteAIDeployment_Call{Call: _e.mock.On("DeleteAIDeployment", ctx, id)} +} + +func (_c *Client_DeleteAIDeployment_Call) Run(run func(ctx context.Context, id string)) *Client_DeleteAIDeployment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Client_DeleteAIDeployment_Call) Return(_a0 error) *Client_DeleteAIDeployment_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Client_DeleteAIDeployment_Call) RunAndReturn(run func(context.Context, string) error) *Client_DeleteAIDeployment_Call { + _c.Call.Return(run) + return _c +} + // DeleteCluster provides a mock function with given fields: ctx, name func (_m *Client) DeleteCluster(ctx context.Context, name string) error { ret := _m.Called(ctx, name) @@ -331,6 +545,125 @@ func (_c *Client_DeleteTeam_Call) RunAndReturn(run func(context.Context, client. return _c } +// GetAIAPIKey provides a mock function with given fields: ctx, deploymentID, name +func (_m *Client) GetAIAPIKey(ctx context.Context, deploymentID string, name string) (*client.AIAPIKey, error) { + ret := _m.Called(ctx, deploymentID, name) + + if len(ret) == 0 { + panic("no return value specified for GetAIAPIKey") + } + + var r0 *client.AIAPIKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*client.AIAPIKey, error)); ok { + return rf(ctx, deploymentID, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *client.AIAPIKey); ok { + r0 = rf(ctx, deploymentID, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIAPIKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, deploymentID, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_GetAIAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAIAPIKey' +type Client_GetAIAPIKey_Call struct { + *mock.Call +} + +// GetAIAPIKey is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +// - name string +func (_e *Client_Expecter) GetAIAPIKey(ctx interface{}, deploymentID interface{}, name interface{}) *Client_GetAIAPIKey_Call { + return &Client_GetAIAPIKey_Call{Call: _e.mock.On("GetAIAPIKey", ctx, deploymentID, name)} +} + +func (_c *Client_GetAIAPIKey_Call) Run(run func(ctx context.Context, deploymentID string, name string)) *Client_GetAIAPIKey_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *Client_GetAIAPIKey_Call) Return(_a0 *client.AIAPIKey, _a1 error) *Client_GetAIAPIKey_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_GetAIAPIKey_Call) RunAndReturn(run func(context.Context, string, string) (*client.AIAPIKey, error)) *Client_GetAIAPIKey_Call { + _c.Call.Return(run) + return _c +} + +// GetAIDeployment provides a mock function with given fields: ctx, name +func (_m *Client) GetAIDeployment(ctx context.Context, name string) (*client.AIDeployment, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetAIDeployment") + } + + var r0 *client.AIDeployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*client.AIDeployment, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *client.AIDeployment); ok { + r0 = rf(ctx, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.AIDeployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_GetAIDeployment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAIDeployment' +type Client_GetAIDeployment_Call struct { + *mock.Call +} + +// GetAIDeployment is a helper method to define mock.On call +// - ctx context.Context +// - name string +func (_e *Client_Expecter) GetAIDeployment(ctx interface{}, name interface{}) *Client_GetAIDeployment_Call { + return &Client_GetAIDeployment_Call{Call: _e.mock.On("GetAIDeployment", ctx, name)} +} + +func (_c *Client_GetAIDeployment_Call) Run(run func(ctx context.Context, name string)) *Client_GetAIDeployment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Client_GetAIDeployment_Call) Return(_a0 *client.AIDeployment, _a1 error) *Client_GetAIDeployment_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_GetAIDeployment_Call) RunAndReturn(run func(context.Context, string) (*client.AIDeployment, error)) *Client_GetAIDeployment_Call { + _c.Call.Return(run) + return _c +} + // GetCluster provides a mock function with given fields: ctx, name func (_m *Client) GetCluster(ctx context.Context, name string) (*client.Cluster, error) { ret := _m.Called(ctx, name) @@ -741,6 +1074,181 @@ func (_c *Client_GetUser_Call) RunAndReturn(run func(context.Context, string) (* return _c } +// ListAIAPIKeys provides a mock function with given fields: ctx, deploymentID +func (_m *Client) ListAIAPIKeys(ctx context.Context, deploymentID string) ([]client.AIAPIKey, error) { + ret := _m.Called(ctx, deploymentID) + + if len(ret) == 0 { + panic("no return value specified for ListAIAPIKeys") + } + + var r0 []client.AIAPIKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]client.AIAPIKey, error)); ok { + return rf(ctx, deploymentID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []client.AIAPIKey); ok { + r0 = rf(ctx, deploymentID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.AIAPIKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, deploymentID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_ListAIAPIKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAIAPIKeys' +type Client_ListAIAPIKeys_Call struct { + *mock.Call +} + +// ListAIAPIKeys is a helper method to define mock.On call +// - ctx context.Context +// - deploymentID string +func (_e *Client_Expecter) ListAIAPIKeys(ctx interface{}, deploymentID interface{}) *Client_ListAIAPIKeys_Call { + return &Client_ListAIAPIKeys_Call{Call: _e.mock.On("ListAIAPIKeys", ctx, deploymentID)} +} + +func (_c *Client_ListAIAPIKeys_Call) Run(run func(ctx context.Context, deploymentID string)) *Client_ListAIAPIKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Client_ListAIAPIKeys_Call) Return(_a0 []client.AIAPIKey, _a1 error) *Client_ListAIAPIKeys_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_ListAIAPIKeys_Call) RunAndReturn(run func(context.Context, string) ([]client.AIAPIKey, error)) *Client_ListAIAPIKeys_Call { + _c.Call.Return(run) + return _c +} + +// ListAIDeployments provides a mock function with given fields: ctx +func (_m *Client) ListAIDeployments(ctx context.Context) ([]client.AIDeployment, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListAIDeployments") + } + + var r0 []client.AIDeployment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]client.AIDeployment, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []client.AIDeployment); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.AIDeployment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_ListAIDeployments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAIDeployments' +type Client_ListAIDeployments_Call struct { + *mock.Call +} + +// ListAIDeployments is a helper method to define mock.On call +// - ctx context.Context +func (_e *Client_Expecter) ListAIDeployments(ctx interface{}) *Client_ListAIDeployments_Call { + return &Client_ListAIDeployments_Call{Call: _e.mock.On("ListAIDeployments", ctx)} +} + +func (_c *Client_ListAIDeployments_Call) Run(run func(ctx context.Context)) *Client_ListAIDeployments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Client_ListAIDeployments_Call) Return(_a0 []client.AIDeployment, _a1 error) *Client_ListAIDeployments_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_ListAIDeployments_Call) RunAndReturn(run func(context.Context) ([]client.AIDeployment, error)) *Client_ListAIDeployments_Call { + _c.Call.Return(run) + return _c +} + +// ListAIModels provides a mock function with given fields: ctx +func (_m *Client) ListAIModels(ctx context.Context) ([]client.AIModel, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListAIModels") + } + + var r0 []client.AIModel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]client.AIModel, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []client.AIModel); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]client.AIModel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_ListAIModels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAIModels' +type Client_ListAIModels_Call struct { + *mock.Call +} + +// ListAIModels is a helper method to define mock.On call +// - ctx context.Context +func (_e *Client_Expecter) ListAIModels(ctx interface{}) *Client_ListAIModels_Call { + return &Client_ListAIModels_Call{Call: _e.mock.On("ListAIModels", ctx)} +} + +func (_c *Client_ListAIModels_Call) Run(run func(ctx context.Context)) *Client_ListAIModels_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Client_ListAIModels_Call) Return(_a0 []client.AIModel, _a1 error) *Client_ListAIModels_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_ListAIModels_Call) RunAndReturn(run func(context.Context) ([]client.AIModel, error)) *Client_ListAIModels_Call { + _c.Call.Return(run) + return _c +} + // ListClusters provides a mock function with given fields: ctx func (_m *Client) ListClusters(ctx context.Context) (client.ClusterList, error) { ret := _m.Called(ctx) diff --git a/pkg/client/ai.go b/pkg/client/ai.go new file mode 100644 index 0000000..5237c0c --- /dev/null +++ b/pkg/client/ai.go @@ -0,0 +1,203 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/url" +) + +type AIModel struct { + ID string `json:"id" yaml:"id"` + DisplayName string `json:"displayName" yaml:"displayName"` + Description string `json:"description" yaml:"description"` + ContextLength int `json:"contextLength" yaml:"contextLength"` +} + +type AIDeploymentCreatedBy struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + UPN string `json:"upn" yaml:"upn"` +} + +type AIDeployment struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Model string `json:"model" yaml:"model"` + Endpoint string `json:"endpoint" yaml:"endpoint"` + CreatedBy AIDeploymentCreatedBy `json:"createdBy" yaml:"createdBy"` +} + +type NewAIDeploymentRequest struct { + Name string `json:"name"` + Model string `json:"model"` +} + +type AIAPIKey struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Prefix string `json:"prefix" yaml:"prefix"` + CreatedBy AIDeploymentCreatedBy `json:"createdBy" yaml:"createdBy"` + CreatedAt string `json:"createdAt" yaml:"createdAt"` + ExpiresAt string `json:"expiresAt" yaml:"expiresAt"` +} + +type AIAPIKeyWithSecret struct { + AIAPIKey + Key string `json:"key" yaml:"key"` +} + +type NewAIAPIKeyRequest struct { + Name string `json:"name"` + TTLDays int `json:"ttlDays"` +} + +func (c *RestClient) CreateAIDeployment(ctx context.Context, request NewAIDeploymentRequest) (*AIDeployment, error) { + body, err := json.Marshal(request) + if err != nil { + return nil, fmt.Errorf("could not marshal request: %w", err) + } + + endpoint := c.baseURIBlurite + "/api/v1/blurite/llm-deployments" + + req, err := c.createAuthenticatedRequest(ctx, "POST", endpoint, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + var deploy AIDeployment + if err = doRequest(c.httpClient, req, &deploy); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return &deploy, nil +} + +func (c *RestClient) ListAIDeployments(ctx context.Context) ([]AIDeployment, error) { + req, err := c.createAuthenticatedRequest(ctx, "GET", c.baseURIBlurite+"/api/v1/blurite/llm-deployments", nil) + if err != nil { + return nil, err + } + + var deployments []AIDeployment + if err = doRequest(c.httpClient, req, &deployments); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return deployments, nil +} + +func (c *RestClient) GetAIDeployment(ctx context.Context, name string) (*AIDeployment, error) { + endpoint := c.baseURIBlurite + "/api/v1/blurite/llm-deployments/by-name/" + url.PathEscape(name) + + req, err := c.createAuthenticatedRequest(ctx, "GET", endpoint, nil) + if err != nil { + return nil, err + } + + var deploy AIDeployment + if err = doRequest(c.httpClient, req, &deploy); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return &deploy, nil +} + +func (c *RestClient) DeleteAIDeployment(ctx context.Context, id string) error { + req, err := c.createAuthenticatedRequest(ctx, "DELETE", c.baseURIBlurite+"/api/v1/blurite/llm-deployments/"+id, nil) + if err != nil { + return err + } + + if err = doRequest[any](c.httpClient, req, nil); err != nil { + return fmt.Errorf("request failed: %w", err) + } + + return nil +} + +func (c *RestClient) ListAIAPIKeys(ctx context.Context, deploymentID string) ([]AIAPIKey, error) { + endpoint := c.baseURIBlurite + "/api/v1/blurite/llm-deployments/" + deploymentID + "/api-keys" + + req, err := c.createAuthenticatedRequest(ctx, "GET", endpoint, nil) + if err != nil { + return nil, err + } + + var keys []AIAPIKey + if err = doRequest(c.httpClient, req, &keys); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return keys, nil +} + +func (c *RestClient) CreateAIAPIKey( + ctx context.Context, deploymentID string, request NewAIAPIKeyRequest, +) (*AIAPIKeyWithSecret, error) { + body, err := json.Marshal(request) + if err != nil { + return nil, fmt.Errorf("could not marshal request: %w", err) + } + + endpoint := c.baseURIBlurite + "/api/v1/blurite/llm-deployments/" + deploymentID + "/api-keys" + + req, err := c.createAuthenticatedRequest(ctx, "POST", endpoint, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + var key AIAPIKeyWithSecret + if err = doRequest(c.httpClient, req, &key); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return &key, nil +} + +func (c *RestClient) GetAIAPIKey(ctx context.Context, deploymentID string, name string) (*AIAPIKey, error) { + endpoint := c.baseURIBlurite + "/api/v1/blurite/llm-deployments/" + + deploymentID + "/api-keys/by-name/" + url.PathEscape(name) + + req, err := c.createAuthenticatedRequest(ctx, "GET", endpoint, nil) + if err != nil { + return nil, err + } + + var key AIAPIKey + if err = doRequest(c.httpClient, req, &key); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return &key, nil +} + +func (c *RestClient) DeleteAIAPIKey(ctx context.Context, deploymentID string, keyID string) error { + endpoint := c.baseURIBlurite + "/api/v1/blurite/llm-deployments/" + deploymentID + "/api-keys/" + keyID + + req, err := c.createAuthenticatedRequest(ctx, "DELETE", endpoint, nil) + if err != nil { + return err + } + + if err = doRequest[any](c.httpClient, req, nil); err != nil { + return fmt.Errorf("request failed: %w", err) + } + + return nil +} + +func (c *RestClient) ListAIModels(ctx context.Context) ([]AIModel, error) { + req, err := c.createAuthenticatedRequest(ctx, "GET", c.baseURIBlurite+"/api/v1/blurite/models", nil) + if err != nil { + return nil, err + } + + var models []AIModel + if err = doRequest(c.httpClient, req, &models); err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + return models, nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index d95f909..5267f16 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -60,6 +60,21 @@ type UserClient interface { GetUser(ctx context.Context, upn string) (*User, error) } +type AIClient interface { + ListAIModels(ctx context.Context) ([]AIModel, error) + ListAIDeployments(ctx context.Context) ([]AIDeployment, error) + GetAIDeployment(ctx context.Context, name string) (*AIDeployment, error) + CreateAIDeployment(ctx context.Context, request NewAIDeploymentRequest) (*AIDeployment, error) + DeleteAIDeployment(ctx context.Context, id string) error +} + +type AIAPIKeyClient interface { + ListAIAPIKeys(ctx context.Context, deploymentID string) ([]AIAPIKey, error) + GetAIAPIKey(ctx context.Context, deploymentID string, name string) (*AIAPIKey, error) + CreateAIAPIKey(ctx context.Context, deploymentID string, request NewAIAPIKeyRequest) (*AIAPIKeyWithSecret, error) + DeleteAIAPIKey(ctx context.Context, deploymentID string, keyID string) error +} + type Client interface { ClusterClient IntegrationClient @@ -67,14 +82,17 @@ type Client interface { TeamsClient UserClient MemberClient + AIClient + AIAPIKeyClient } type RestClientOption func(*RestClient) type RestClient struct { - baseURI string - httpClient *http.Client - authenticator *authenticator.Authenticator + baseURI string + baseURIBlurite string + httpClient *http.Client + authenticator *authenticator.Authenticator } var _ Client = New() @@ -85,9 +103,10 @@ func New(options ...RestClientOption) *RestClient { Timeout: defaultHTTPTimeout, } restClient := &RestClient{ - baseURI: build.PlatformAPIHost(), - httpClient: client, - authenticator: authenticator.NewAuthenticator(authenticator.ConfigFromBuildProps()), + baseURI: build.PlatformAPIHost(), + baseURIBlurite: build.PlatformAPIHostBlurite(), + httpClient: client, + authenticator: authenticator.NewAuthenticator(authenticator.ConfigFromBuildProps()), } for _, opt := range options { diff --git a/pkg/clientset/clientset.go b/pkg/clientset/clientset.go index 58d402e..83eed65 100644 --- a/pkg/clientset/clientset.go +++ b/pkg/clientset/clientset.go @@ -21,6 +21,7 @@ const homeAccountIDParts = 2 var ( errNotAuthenticatedPreHook = errors.New("you need to sign in before executing this operation") errInvalidHomeAccountID = errors.New("invalid HomeAccountID format") + errFeatureNotAvailable = errors.New("this feature is not available for your tenant") ) type Authenticator interface { @@ -108,6 +109,32 @@ func fqcn(cmd *cobra.Command) string { return name } +// EnsureAITenantPreHook is a pre-run hook that checks if the current tenant +// has access to AI features. It composes EnsureSignedIn so auth is checked first. +func (c *ClientSet) EnsureAITenantPreHook(cmd *cobra.Command, args []string) error { + return c.PreHooks(c.EnsureSignedIn, c.ensureAITenant)(cmd, args) +} + +func (c *ClientSet) ensureAITenant(cmd *cobra.Command, _ []string) error { + allowedTenants := map[string]bool{ + "9b5ff18e-53c0-45a2-8bc2-9c0c8f60b2c6": true, // intility + "f0a51c83-6cc9-4830-9d01-d4d18c068e32": true, // skoglab + } + + tenantID, err := c.GetTenantID(cmd.Context()) + if err != nil { + return fmt.Errorf("could not determine tenant: %w", err) + } + + if !allowedTenants[tenantID] { + cmd.SilenceUsage = true + + return errFeatureNotAvailable + } + + return nil +} + // GetTenantID extracts the tenant ID from the current account's HomeAccountID. // The HomeAccountID format is "." where tid is the tenant ID. func (c *ClientSet) GetTenantID(ctx context.Context) (string, error) { diff --git a/pkg/commands/account/login.go b/pkg/commands/account/login.go index 99757a0..a9ed0bd 100644 --- a/pkg/commands/account/login.go +++ b/pkg/commands/account/login.go @@ -55,7 +55,7 @@ func NewLoginCommand(set clientset.ClientSet) *cobra.Command { return redact.Errorf("could not authenticate: %w", err) } - ux.Fsuccessf(cmd.OutOrStdout(), "authenticated as %s", result.Account.PreferredUsername) + ux.Fsuccessf(cmd.OutOrStdout(), "authenticated as %s\n", result.Account.PreferredUsername) return nil }, diff --git a/pkg/commands/ai/apikey/create.go b/pkg/commands/ai/apikey/create.go new file mode 100644 index 0000000..af1edef --- /dev/null +++ b/pkg/commands/ai/apikey/create.go @@ -0,0 +1,116 @@ +package apikey + +import ( + "regexp" + + "github.com/spf13/cobra" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/client" + "github.com/intility/indev/pkg/clientset" +) + +const ( + maxNameLength = 50 + minNameLength = 3 + validNameRegex = "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" +) + +var ( + errEmptyName = redact.Errorf("API key name cannot be empty") + errEmptyDeployment = redact.Errorf("deployment name must be specified") + errInvalidTTL = redact.Errorf("TTL is required and must be a positive integer in the range 1 - 365") + errInvalidNameLength = redact.Errorf( + "API key name must be between %d and %d characters long", + minNameLength, maxNameLength, + ) + errInvalidNameFormat = redact.Errorf("API key name must match the pattern %s", validNameRegex) +) + +type CreateOptions struct { + Name string + Deployment string + TTLDays int +} + +func NewCreateCommand(set clientset.ClientSet) *cobra.Command { + var options CreateOptions + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new API key for an AI deployment", + Long: "Create a new API key for an AI deployment", + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "apikey.create") + defer span.End() + + cmd.SilenceUsage = true + + err := validateCreateOptions(options) + if err != nil { + return err + } + + deploy, err := set.PlatformClient.GetAIDeployment(ctx, options.Deployment) + if err != nil { + return redact.Errorf("could not find AI deployment: %w", redact.Safe(err)) + } + + key, err := set.PlatformClient.CreateAIAPIKey(ctx, deploy.ID, client.NewAIAPIKeyRequest{ + Name: options.Name, + TTLDays: options.TTLDays, + }) + if err != nil { + return redact.Errorf("could not create API key: %w", redact.Safe(err)) + } + + ux.Fsuccessf(cmd.OutOrStdout(), "API key created successfully\n") + ux.Fprintf(cmd.OutOrStdout(), "\n %s\n\n", key.Key) + ux.Fwarningf(cmd.OutOrStdout(), "Store this key securely — it will not be shown again.\n") + + return nil + }, + } + + cmd.Flags().StringVarP(&options.Name, + "name", "n", "", "Name of the API key to create") + + cmd.Flags().StringVarP(&options.Deployment, + "deployment", "d", "", "Name of the AI deployment") + + cmd.Flags().IntVarP(&options.TTLDays, + "ttl", "t", 0, "Number of days the API key will be valid (1-365)") + + _ = cmd.MarkFlagRequired("deployment") + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("ttl") + + return cmd +} + +func validateCreateOptions(options CreateOptions) error { + if options.Deployment == "" { + return errEmptyDeployment + } + + if options.Name == "" { + return errEmptyName + } + + if options.TTLDays <= 0 || options.TTLDays > 365 { + return errInvalidTTL + } + + if len(options.Name) < minNameLength || len(options.Name) > maxNameLength { + return errInvalidNameLength + } + + if matched, err := regexp.MatchString(validNameRegex, options.Name); err != nil || !matched { + return errInvalidNameFormat + } + + return nil +} diff --git a/pkg/commands/ai/apikey/create_test.go b/pkg/commands/ai/apikey/create_test.go new file mode 100644 index 0000000..8b65df1 --- /dev/null +++ b/pkg/commands/ai/apikey/create_test.go @@ -0,0 +1,202 @@ +package apikey + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateCreateOptions(t *testing.T) { + tests := []struct { + name string + options CreateOptions + wantErr error + }{ + // Empty field validation + { + name: "empty deployment returns error", + options: CreateOptions{ + Deployment: "", + Name: "my-key", + TTLDays: 30, + }, + wantErr: errEmptyDeployment, + }, + { + name: "empty name returns error", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "", + TTLDays: 30, + }, + wantErr: errEmptyName, + }, + + // TTL validation + { + name: "zero TTL returns error", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: 0, + }, + wantErr: errInvalidTTL, + }, + { + name: "negative TTL returns error", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: -1, + }, + wantErr: errInvalidTTL, + }, + { + name: "TTL above 365 returns error", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: 366, + }, + wantErr: errInvalidTTL, + }, + { + name: "TTL at 1 is valid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: 1, + }, + wantErr: nil, + }, + { + name: "TTL at 365 is valid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: 365, + }, + wantErr: nil, + }, + + // Name length validation + { + name: "name at minimum length (3)", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "abc", + TTLDays: 30, + }, + wantErr: nil, + }, + { + name: "name below minimum length (2)", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "ab", + TTLDays: 30, + }, + wantErr: errInvalidNameLength, + }, + { + name: "name at maximum length (50)", + options: CreateOptions{ + Deployment: "my-deployment", + Name: strings.Repeat("a", 50), + TTLDays: 30, + }, + wantErr: nil, + }, + { + name: "name above maximum length (51)", + options: CreateOptions{ + Deployment: "my-deployment", + Name: strings.Repeat("a", 51), + TTLDays: 30, + }, + wantErr: errInvalidNameLength, + }, + + // Name format validation - valid patterns + { + name: "name with hyphens is valid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: 30, + }, + wantErr: nil, + }, + { + name: "name with numbers is valid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "key123", + TTLDays: 30, + }, + wantErr: nil, + }, + + // Name format validation - invalid patterns + { + name: "name starting with hyphen is invalid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "-my-key", + TTLDays: 30, + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name ending with hyphen is invalid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key-", + TTLDays: 30, + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name with uppercase is invalid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "MyKey", + TTLDays: 30, + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name with special characters is invalid", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my@key", + TTLDays: 30, + }, + wantErr: errInvalidNameFormat, + }, + + // Valid options + { + name: "valid options pass", + options: CreateOptions{ + Deployment: "my-deployment", + Name: "my-key", + TTLDays: 30, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCreateOptions(tt.options) + + if tt.wantErr == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.wantErr) + } + }) + } +} diff --git a/pkg/commands/ai/apikey/delete.go b/pkg/commands/ai/apikey/delete.go new file mode 100644 index 0000000..eb23837 --- /dev/null +++ b/pkg/commands/ai/apikey/delete.go @@ -0,0 +1,60 @@ +package apikey + +import ( + "github.com/spf13/cobra" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/clientset" +) + +func NewDeleteCommand(set clientset.ClientSet) *cobra.Command { + var deployment string + + var name string + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete an API key", + Long: "Delete an API key from an AI deployment", + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "apikey.delete") + defer span.End() + + cmd.SilenceUsage = true + + if deployment == "" { + return redact.Errorf("deployment name must be specified") + } + + if name == "" { + return redact.Errorf("API key name must be specified") + } + + deploy, err := set.PlatformClient.GetAIDeployment(ctx, deployment) + if err != nil { + return redact.Errorf("could not find AI deployment: %w", redact.Safe(err)) + } + + key, err := set.PlatformClient.GetAIAPIKey(ctx, deploy.ID, name) + if err != nil { + return redact.Errorf("could not find API key: %w", redact.Safe(err)) + } + + if err = set.PlatformClient.DeleteAIAPIKey(ctx, deploy.ID, key.ID); err != nil { + return redact.Errorf("could not delete API key: %w", redact.Safe(err)) + } + + ux.Fsuccessf(cmd.OutOrStdout(), "deleted API key: %s\n", name) + + return nil + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "Name of the API key to delete") + cmd.Flags().StringVarP(&deployment, "deployment", "d", "", "Name of the AI deployment") + + return cmd +} diff --git a/pkg/commands/ai/apikey/list.go b/pkg/commands/ai/apikey/list.go new file mode 100644 index 0000000..e636a68 --- /dev/null +++ b/pkg/commands/ai/apikey/list.go @@ -0,0 +1,111 @@ +package apikey + +import ( + "encoding/json" + "io" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/client" + "github.com/intility/indev/pkg/clientset" + "github.com/intility/indev/pkg/outputformat" +) + +func NewListCommand(set clientset.ClientSet) *cobra.Command { + var deployment string + + output := outputformat.Format("") + + cmd := &cobra.Command{ + Use: "list", + Short: "List API keys", + Long: "List all API keys for an AI deployment", + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "apikey.list") + defer span.End() + + cmd.SilenceUsage = true + + if deployment == "" { + return redact.Errorf("deployment name must be specified") + } + + deploy, err := set.PlatformClient.GetAIDeployment(ctx, deployment) + if err != nil { + return redact.Errorf("could not find AI deployment: %w", redact.Safe(err)) + } + + keys, err := set.PlatformClient.ListAIAPIKeys(ctx, deploy.ID) + if err != nil { + return redact.Errorf("could not list API keys: %w", redact.Safe(err)) + } + + if len(keys) == 0 { + ux.Fprintf(cmd.OutOrStdout(), "No API keys found\n") + return nil + } + + if err = printAPIKeyList(cmd.OutOrStdout(), output, keys); err != nil { + return redact.Errorf("could not print API keys: %w", redact.Safe(err)) + } + + return nil + }, + } + + cmd.Flags().StringVarP(&deployment, "deployment", "d", "", "Name of the AI deployment") + cmd.Flags().VarP(&output, "output", "o", "Output format (wide, json, yaml)") + + return cmd +} + +func printAPIKeyList(writer io.Writer, format outputformat.Format, keys []client.AIAPIKey) error { + var err error + + switch format { + case "json": + enc := json.NewEncoder(writer) + enc.SetIndent("", " ") + err = enc.Encode(keys) + case "yaml": + indent := 2 + enc := yaml.NewEncoder(writer) + enc.SetIndent(indent) + err = enc.Encode(keys) + case "wide": + table := ux.TableFromObjects(keys, func(k client.AIAPIKey) []ux.Row { + return []ux.Row{ + ux.NewRow("Name", k.Name), + ux.NewRow("Prefix", k.Prefix), + ux.NewRow("Created By", k.CreatedBy.Name), + ux.NewRow("Created At", k.CreatedAt), + ux.NewRow("Expires At", k.ExpiresAt), + ux.NewRow("ID", k.ID), + } + }) + + ux.Fprintf(writer, "%s", table.String()) + default: + table := ux.TableFromObjects(keys, func(k client.AIAPIKey) []ux.Row { + return []ux.Row{ + ux.NewRow("Name", k.Name), + ux.NewRow("Prefix", k.Prefix), + ux.NewRow("Created At", k.CreatedAt), + ux.NewRow("Expires At", k.ExpiresAt), + } + }) + + ux.Fprintf(writer, "%s", table.String()) + } + + if err != nil { + return redact.Errorf("output encoder failed: %w", redact.Safe(err)) + } + + return nil +} diff --git a/pkg/commands/ai/deployment/create.go b/pkg/commands/ai/deployment/create.go new file mode 100644 index 0000000..11f7675 --- /dev/null +++ b/pkg/commands/ai/deployment/create.go @@ -0,0 +1,100 @@ +package deployment + +import ( + "regexp" + + "github.com/spf13/cobra" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/client" + "github.com/intility/indev/pkg/clientset" +) + +const ( + maxNameLength = 50 + minNameLength = 3 + validNameRegex = "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" +) + +var ( + errEmptyName = redact.Errorf("deployment name cannot be empty") + errEmptyModel = redact.Errorf("model must be specified") + errInvalidNameLength = redact.Errorf( + "deployment name must be between %d and %d characters long", + minNameLength, maxNameLength, + ) + errInvalidNameFormat = redact.Errorf("deployment name must match the pattern %s", validNameRegex) +) + +type CreateOptions struct { + Name string + Model string +} + +func NewCreateCommand(set clientset.ClientSet) *cobra.Command { + var options CreateOptions + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new AI deployment", + Long: "Create a new AI deployment with specified model", + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "aideployment.create") + defer span.End() + + cmd.SilenceUsage = true + + err := validateCreateOptions(options) + if err != nil { + return err + } + + aideployment, err := set.PlatformClient.CreateAIDeployment(ctx, client.NewAIDeploymentRequest{ + Name: options.Name, + Model: options.Model, + }) + if err != nil { + return redact.Errorf("could not create AI deployment: %w", redact.Safe(err)) + } + + ux.Fsuccessf(cmd.OutOrStdout(), "created AI deployment: %s\n", aideployment.Name) + ux.Fprintf(cmd.OutOrStdout(), "\n endpoint: %s\n\n", aideployment.Endpoint) + + return nil + }, + } + + cmd.Flags().StringVarP(&options.Name, + "name", "n", "", "Name of the deployment to create") + + cmd.Flags().StringVarP(&options.Model, + "model", "m", "", "ID of the AI model to use. Available models can be found using 'indev ai model list'") + + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("model") + + return cmd +} + +func validateCreateOptions(options CreateOptions) error { + if options.Name == "" { + return errEmptyName + } + + if options.Model == "" { + return errEmptyModel + } + + if len(options.Name) < minNameLength || len(options.Name) > maxNameLength { + return errInvalidNameLength + } + + if matched, err := regexp.MatchString(validNameRegex, options.Name); err != nil || !matched { + return errInvalidNameFormat + } + + return nil +} diff --git a/pkg/commands/ai/deployment/create_test.go b/pkg/commands/ai/deployment/create_test.go new file mode 100644 index 0000000..2e2dc11 --- /dev/null +++ b/pkg/commands/ai/deployment/create_test.go @@ -0,0 +1,150 @@ +package deployment + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateCreateOptions(t *testing.T) { + tests := []struct { + name string + options CreateOptions + wantErr error + }{ + // Empty field validation + { + name: "empty name returns error", + options: CreateOptions{ + Name: "", + Model: "gpt-4o", + }, + wantErr: errEmptyName, + }, + { + name: "empty model returns error", + options: CreateOptions{ + Name: "my-deployment", + Model: "", + }, + wantErr: errEmptyModel, + }, + + // Name length validation + { + name: "name at minimum length (3)", + options: CreateOptions{ + Name: "abc", + Model: "gpt-4o", + }, + wantErr: nil, + }, + { + name: "name below minimum length (2)", + options: CreateOptions{ + Name: "ab", + Model: "gpt-4o", + }, + wantErr: errInvalidNameLength, + }, + { + name: "name at maximum length (50)", + options: CreateOptions{ + Name: strings.Repeat("a", 50), + Model: "gpt-4o", + }, + wantErr: nil, + }, + { + name: "name above maximum length (51)", + options: CreateOptions{ + Name: strings.Repeat("a", 51), + Model: "gpt-4o", + }, + wantErr: errInvalidNameLength, + }, + + // Name format validation - valid patterns + { + name: "name with hyphens is valid", + options: CreateOptions{ + Name: "my-deployment", + Model: "gpt-4o", + }, + wantErr: nil, + }, + { + name: "name with numbers is valid", + options: CreateOptions{ + Name: "deploy123", + Model: "gpt-4o", + }, + wantErr: nil, + }, + + // Name format validation - invalid patterns + { + name: "name starting with hyphen is invalid", + options: CreateOptions{ + Name: "-deployment", + Model: "gpt-4o", + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name ending with hyphen is invalid", + options: CreateOptions{ + Name: "deployment-", + Model: "gpt-4o", + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name with uppercase is invalid", + options: CreateOptions{ + Name: "MyDeployment", + Model: "gpt-4o", + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name with special characters is invalid", + options: CreateOptions{ + Name: "my@deployment", + Model: "gpt-4o", + }, + wantErr: errInvalidNameFormat, + }, + { + name: "name with underscore is invalid", + options: CreateOptions{ + Name: "my_deployment", + Model: "gpt-4o", + }, + wantErr: errInvalidNameFormat, + }, + + // Valid options + { + name: "valid options pass", + options: CreateOptions{ + Name: "my-deployment", + Model: "gpt-4o", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCreateOptions(tt.options) + + if tt.wantErr == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.wantErr) + } + }) + } +} diff --git a/pkg/commands/ai/deployment/delete.go b/pkg/commands/ai/deployment/delete.go new file mode 100644 index 0000000..691814a --- /dev/null +++ b/pkg/commands/ai/deployment/delete.go @@ -0,0 +1,48 @@ +package deployment + +import ( + "github.com/spf13/cobra" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/clientset" +) + +func NewDeleteCommand(set clientset.ClientSet) *cobra.Command { + var name string + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete an AI deployment", + Long: "Delete an AI deployment from the Intility Developer Platform", + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "aideployment.delete") + defer span.End() + + cmd.SilenceUsage = true + + if name == "" { + return redact.Errorf("deployment name must be specified") + } + + deploy, err := set.PlatformClient.GetAIDeployment(ctx, name) + if err != nil { + return redact.Errorf("could not find AI deployment: %w", redact.Safe(err)) + } + + if err = set.PlatformClient.DeleteAIDeployment(ctx, deploy.ID); err != nil { + return redact.Errorf("could not delete AI deployment: %w", redact.Safe(err)) + } + + ux.Fsuccessf(cmd.OutOrStdout(), "deleted AI deployment: %s\n", name) + + return nil + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "Name of the deployment to delete") + + return cmd +} diff --git a/pkg/commands/ai/deployment/list.go b/pkg/commands/ai/deployment/list.go new file mode 100644 index 0000000..8ebd884 --- /dev/null +++ b/pkg/commands/ai/deployment/list.go @@ -0,0 +1,97 @@ +package deployment + +import ( + "encoding/json" + "io" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/client" + "github.com/intility/indev/pkg/clientset" + "github.com/intility/indev/pkg/outputformat" +) + +func NewListCommand(set clientset.ClientSet) *cobra.Command { + output := outputformat.Format("") + cmd := &cobra.Command{ + Use: "list", + Short: "List AI deployments", + Long: "List all AI deployments in the Intility Developer Platform", + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "aideployment.list") + defer span.End() + + cmd.SilenceUsage = true + + deployments, err := set.PlatformClient.ListAIDeployments(ctx) + if err != nil { + return redact.Errorf("could not list AI deployments: %w", redact.Safe(err)) + } + + if len(deployments) == 0 { + ux.Fprintf(cmd.OutOrStdout(), "No AI deployments found\n") + return nil + } + + if err = printDeploymentList(cmd.OutOrStdout(), output, deployments); err != nil { + return redact.Errorf("could not print AI deployments: %w", redact.Safe(err)) + } + + return nil + }, + } + + cmd.Flags().VarP(&output, "output", "o", "Output format (wide, json, yaml)") + + return cmd +} + +func printDeploymentList(writer io.Writer, format outputformat.Format, deployments []client.AIDeployment) error { + var err error + + switch format { + case "json": + enc := json.NewEncoder(writer) + enc.SetIndent("", " ") + err = enc.Encode(deployments) + case "yaml": + indent := 2 + enc := yaml.NewEncoder(writer) + enc.SetIndent(indent) + err = enc.Encode(deployments) + case "wide": + table := ux.TableFromObjects(deployments, func(d client.AIDeployment) []ux.Row { + return []ux.Row{ + ux.NewRow("Name", d.Name), + ux.NewRow("Model", d.Model), + ux.NewRow("Endpoint", d.Endpoint), + ux.NewRow("Created By", d.CreatedBy.Name), + ux.NewRow("Created By UPN", d.CreatedBy.UPN), + ux.NewRow("ID", d.ID), + } + }) + + ux.Fprintf(writer, "%s", table.String()) + default: + table := ux.TableFromObjects(deployments, func(d client.AIDeployment) []ux.Row { + return []ux.Row{ + ux.NewRow("Name", d.Name), + ux.NewRow("Model", d.Model), + ux.NewRow("Endpoint", d.Endpoint), + } + }) + + ux.Fprintf(writer, "%s", table.String()) + } + + if err != nil { + return redact.Errorf("output encoder failed: %w", redact.Safe(err)) + } + + return nil +} diff --git a/pkg/commands/ai/models_list.go b/pkg/commands/ai/models_list.go new file mode 100644 index 0000000..691e950 --- /dev/null +++ b/pkg/commands/ai/models_list.go @@ -0,0 +1,96 @@ +package ai + +import ( + "encoding/json" + "io" + "strconv" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/intility/indev/internal/redact" + "github.com/intility/indev/internal/telemetry" + "github.com/intility/indev/internal/ux" + "github.com/intility/indev/pkg/client" + "github.com/intility/indev/pkg/clientset" + "github.com/intility/indev/pkg/outputformat" +) + +func NewListCommand(set clientset.ClientSet) *cobra.Command { + output := outputformat.Format("") + cmd := &cobra.Command{ + Use: "list", + Short: "List all AI models", + Long: `List all available AI models in the Intility Developer Platform`, + PreRunE: set.EnsureSignedInPreHook, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, span := telemetry.StartSpan(cmd.Context(), "aimodels.list") + defer span.End() + + cmd.SilenceUsage = true + + models, err := set.PlatformClient.ListAIModels(ctx) + if err != nil { + return redact.Errorf("could not list AI models: %w", redact.Safe(err)) + } + + if len(models) == 0 { + ux.Fprintf(cmd.OutOrStdout(), "No AI models found\n") + return nil + } + + if err = printAIModelsList(cmd.OutOrStdout(), output, models); err != nil { + return redact.Errorf("could not print ai models list: %w", redact.Safe(err)) + } + + return nil + }, + } + + cmd.Flags().VarP(&output, "output", "o", "Output format (wide, json, yaml)") + + return cmd +} + +func printAIModelsList(writer io.Writer, format outputformat.Format, aimodels []client.AIModel) error { + var err error + + switch format { + case "json": + enc := json.NewEncoder(writer) + enc.SetIndent("", " ") + err = enc.Encode(aimodels) + case "yaml": + indent := 2 + enc := yaml.NewEncoder(writer) + enc.SetIndent(indent) + err = enc.Encode(aimodels) + case "wide": + table := ux.TableFromObjects(aimodels, func(aimodel client.AIModel) []ux.Row { + return []ux.Row{ + ux.NewRow("Name", aimodel.DisplayName), + ux.NewRow("ID", aimodel.ID), + ux.NewRow("Description", aimodel.Description), + ux.NewRow("Context Length", strconv.Itoa(aimodel.ContextLength)), + } + }) + + ux.Fprintf(writer, "%s", table.String()) + default: + table := ux.TableFromObjects(aimodels, func(aimodel client.AIModel) []ux.Row { + return []ux.Row{ + ux.NewRow("Name", aimodel.DisplayName), + ux.NewRow("ID", aimodel.ID), + ux.NewRow("Context Length", strconv.Itoa(aimodel.ContextLength)), + } + }) + + ux.Fprintf(writer, "%s", table.String()) + } + + if err != nil { + return redact.Errorf("output encoder failed: %w", redact.Safe(err)) + } + + return nil +} diff --git a/pkg/rootcommand/rootcommand.go b/pkg/rootcommand/rootcommand.go index 7fb4ff7..3d30cb9 100644 --- a/pkg/rootcommand/rootcommand.go +++ b/pkg/rootcommand/rootcommand.go @@ -10,6 +10,9 @@ import ( "github.com/intility/indev/pkg/client" "github.com/intility/indev/pkg/clientset" "github.com/intility/indev/pkg/commands/account" + "github.com/intility/indev/pkg/commands/ai" + "github.com/intility/indev/pkg/commands/ai/apikey" + "github.com/intility/indev/pkg/commands/ai/deployment" "github.com/intility/indev/pkg/commands/cluster" "github.com/intility/indev/pkg/commands/cluster/access" "github.com/intility/indev/pkg/commands/teams" @@ -38,6 +41,7 @@ func GetRootCommand() *cobra.Command { rootCmd.AddCommand(getAccountCommand(clients)) rootCmd.AddCommand(getTeamsCommand(clients)) rootCmd.AddCommand(getUserCommand(clients)) + rootCmd.AddCommand(getAICommand(clients)) return rootCmd } @@ -149,6 +153,66 @@ func getUserCommand(set clientset.ClientSet) *cobra.Command { return cmd } +func getAICommand(set clientset.ClientSet) *cobra.Command { + cmd := &cobra.Command{ + Use: "ai", + Short: "Manage Intility Developer Platform AI Deployments", + Long: "Manage Intility Developer Platform AI Deployments", + Hidden: true, + PersistentPreRunE: set.EnsureAITenantPreHook, + Run: showHelp, + } + + cmd.AddCommand(getAIModelCommand(set)) + cmd.AddCommand(getAIDeploymentCommand(set)) + cmd.AddCommand(getAIAPIKeyCommand(set)) + + return cmd +} + +func getAIModelCommand(set clientset.ClientSet) *cobra.Command { + cmd := &cobra.Command{ + Use: "model", + Short: "Manage AI models", + Long: "Manage AI models", + Run: showHelp, + } + + cmd.AddCommand(ai.NewListCommand(set)) + + return cmd +} + +func getAIDeploymentCommand(set clientset.ClientSet) *cobra.Command { + cmd := &cobra.Command{ + Use: "deployment", + Short: "Manage AI deployments", + Long: "Manage AI deployments", + Run: showHelp, + } + + cmd.AddCommand(deployment.NewCreateCommand(set)) + cmd.AddCommand(deployment.NewListCommand(set)) + cmd.AddCommand(deployment.NewDeleteCommand(set)) + + return cmd +} + +func getAIAPIKeyCommand(set clientset.ClientSet) *cobra.Command { + cmd := &cobra.Command{ + Use: "apikey", + Short: "Manage AI deployment API keys", + Long: "Manage AI deployment API keys", + Run: showHelp, + } + + cmd.AddCommand(apikey.NewCreateCommand(set)) + cmd.AddCommand(apikey.NewListCommand(set)) + cmd.AddCommand(apikey.NewDeleteCommand(set)) + + return cmd +} + func showHelp(cmd *cobra.Command, args []string) { _, span := telemetry.StartSpan(cmd.Context(), cmd.Use) defer span.End()