Remove mothership from this repo in preparation of OpenELA move

This commit is contained in:
Mustafa Gezen 2023-11-04 21:02:15 +01:00
parent f759b86f85
commit 943a0d60bd
Signed by: mustafa
GPG Key ID: DCDF010D946438C1
101 changed files with 0 additions and 9289 deletions

View File

@ -1,27 +0,0 @@
# Copyright 2023 Peridot Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load("//devtools/taskrunner2:defs.bzl", "taskrunner2")
taskrunner2(
name = "mothership",
dev_frontend_flags = True,
targets = [
"//devtools/devtemporal",
"//devtools/devdex",
],
watch_targets = [
"//tools/mothership/cmd/mship_dev",
],
)

View File

@ -1,74 +0,0 @@
# Copyright 2023 Peridot Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "rpc",
srcs = [
"operation.go",
"rescue.go",
"retract.go",
"rpc.go",
"worker.go",
],
importpath = "go.resf.org/peridot/tools/mothership/admin/rpc",
visibility = ["//visibility:public"],
deps = [
"//base/go",
"//third_party/googleapis/google/longrunning:longrunning_go_proto",
"//tools/mothership/db",
"//tools/mothership/proto/admin/v1:pb",
"//tools/mothership/proto/v1:pb",
"//tools/mothership/worker_server",
"//vendor/go.ciq.dev/pika",
"//vendor/go.temporal.io/api/enums/v1:enums",
"//vendor/go.temporal.io/api/serviceerror",
"//vendor/go.temporal.io/api/workflowservice/v1:workflowservice",
"//vendor/go.temporal.io/sdk/client",
"@go_googleapis//google/rpc:code_go_proto",
"@go_googleapis//google/rpc:errdetails_go_proto",
"@go_googleapis//google/rpc:status_go_proto",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//status",
"@org_golang_google_protobuf//types/known/anypb",
"@org_golang_google_protobuf//types/known/emptypb",
"@org_golang_google_protobuf//types/known/timestamppb",
],
)
go_test(
name = "rpc_test",
size = "small",
srcs = [
"main_test.go",
"worker_test.go",
],
embed = [":rpc"],
deps = [
"//base/go",
"//tools/mothership/db",
"//tools/mothership/migrations",
"//tools/mothership/proto/admin/v1:pb",
"//vendor/github.com/stretchr/testify/require",
"//vendor/github.com/testcontainers/testcontainers-go",
"//vendor/github.com/testcontainers/testcontainers-go/modules/postgres",
"//vendor/github.com/testcontainers/testcontainers-go/wait",
"//vendor/go.temporal.io/sdk/client",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//metadata",
"@org_golang_google_grpc//status",
],
)

View File

@ -1,104 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
"context"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
base "go.resf.org/peridot/base/go"
"go.resf.org/peridot/tools/mothership/migrations"
"go.temporal.io/sdk/client"
"google.golang.org/grpc/metadata"
"os"
"testing"
"time"
)
var (
s *Server
userInfo base.UserInfo
)
type fakeTemporalClient struct {
client.Client
}
func TestMain(m *testing.M) {
// Create temporary file
dir, err := os.MkdirTemp("", "test-db-*")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
scripts, err := base.EmbedFSToOSFS(dir, mothership_migrations.UpSQLs, ".")
if err != nil {
panic(err)
}
ctx := context.Background()
pgContainer, err := postgres.RunContainer(
ctx,
testcontainers.WithImage("postgres:15.3-alpine"),
postgres.WithInitScripts(scripts...),
postgres.WithDatabase("mshiptest"),
postgres.WithUsername("postgres"),
postgres.WithPassword("postgres"),
testcontainers.WithWaitStrategy(
wait.
ForLog("database system is ready to accept connections").
WithOccurrence(2).WithStartupTimeout(5*time.Second),
),
)
if err != nil {
panic(err)
}
defer pgContainer.Terminate(ctx)
connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
if err != nil {
panic(err)
}
db, err := base.NewDB(connStr)
if err != nil {
panic(err)
}
provider := base.NewTestOidcProvider(&userInfo)
interceptorDetails := &base.OidcInterceptorDetails{
Provider: provider,
Group: "",
}
s, err = NewServer(db, &fakeTemporalClient{}, interceptorDetails)
if err != nil {
panic(err)
}
os.Exit(m.Run())
}
func testContext() context.Context {
mdMap := map[string]string{}
if userInfo != nil {
mdMap["authorization"] = "bearer " + userInfo.Subject()
}
md := metadata.New(mdMap)
ctx := metadata.NewIncomingContext(context.Background(), md)
return context.WithValue(ctx, "user", userInfo)
}

View File

@ -1,144 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
"context"
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
v11 "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
"google.golang.org/genproto/googleapis/longrunning"
rpccode "google.golang.org/genproto/googleapis/rpc/code"
rpcstatus "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *Server) describeWorkflowToOperation(ctx context.Context, res *workflowservice.DescribeWorkflowExecutionResponse) (*longrunning.Operation, error) {
if res.WorkflowExecutionInfo == nil {
return nil, status.Error(codes.NotFound, "workflow not found")
}
if res.WorkflowExecutionInfo.Execution == nil {
return nil, status.Error(codes.NotFound, "workflow not found")
}
op := &longrunning.Operation{
Name: res.WorkflowExecutionInfo.Execution.WorkflowId,
}
// If the workflow is not running, we can mark the operation as done
if res.WorkflowExecutionInfo.Status != v11.WORKFLOW_EXECUTION_STATUS_RUNNING {
op.Done = true
}
// Add metadata
rpmMetadata := &mshipadminpb.RetractEntryMetadata{
StartTime: nil,
EndTime: nil,
}
st := res.WorkflowExecutionInfo.GetStartTime()
if st != nil {
rpmMetadata.StartTime = timestamppb.New(*st)
}
et := res.WorkflowExecutionInfo.GetCloseTime()
if et != nil {
rpmMetadata.EndTime = timestamppb.New(*et)
}
rpmMetadataAny, err := anypb.New(rpmMetadata)
if err != nil {
return op, nil
}
op.Metadata = rpmMetadataAny
// If completed, add result
// If failed, add error
if res.WorkflowExecutionInfo.Status == v11.WORKFLOW_EXECUTION_STATUS_COMPLETED {
// Complete, we need to get the result using GetWorkflow
run := s.temporal.GetWorkflow(ctx, op.Name, "")
var res mothershippb.ProcessRPMResponse
if err := run.Get(ctx, &res); err != nil {
return nil, err
}
resAny, err := anypb.New(&res)
if err != nil {
return nil, err
}
op.Result = &longrunning.Operation_Response{Response: resAny}
} else if res.WorkflowExecutionInfo.Status == v11.WORKFLOW_EXECUTION_STATUS_FAILED {
// Failed, we need to get the error using GetWorkflow
run := s.temporal.GetWorkflow(ctx, op.Name, "")
err := run.Get(ctx, nil)
// No error so return with a generic error
if err == nil {
op.Result = &longrunning.Operation_Error{
Error: &rpcstatus.Status{
Code: int32(rpccode.Code_INTERNAL),
Message: "workflow failed",
},
}
return op, nil
}
// Error, so return with the error
op.Result = &longrunning.Operation_Error{
Error: &rpcstatus.Status{
Code: int32(rpccode.Code_FAILED_PRECONDITION),
Message: err.Error(),
},
}
} else if res.WorkflowExecutionInfo.Status == v11.WORKFLOW_EXECUTION_STATUS_CANCELED {
// Error, so return with the error
op.Result = &longrunning.Operation_Error{
Error: &rpcstatus.Status{
Code: int32(rpccode.Code_CANCELLED),
Message: "workflow canceled",
},
}
}
return op, nil
}
func (s *Server) getOperation(ctx context.Context, name string) (*longrunning.Operation, error) {
res, err := s.temporal.DescribeWorkflowExecution(ctx, name, "")
if err != nil {
if _, ok := err.(*serviceerror.NotFound); ok {
return nil, status.Error(codes.NotFound, "workflow not found")
}
// Log error, but user doesn't need to know about it
base.LogErrorf("failed to describe workflow: %v", err)
return &longrunning.Operation{
Name: name,
}, nil
}
return s.describeWorkflowToOperation(ctx, res)
}
func (s *Server) GetOperation(ctx context.Context, req *longrunning.GetOperationRequest) (*longrunning.Operation, error) {
// Get from Temporal. We don't care about long term storage, so we don't
// need to store the operation in the database.
return s.getOperation(ctx, req.Name)
}

View File

@ -1,66 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
"context"
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothership_db "go.resf.org/peridot/tools/mothership/db"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"strings"
)
func (s *Server) RescueEntryImport(ctx context.Context, req *mshipadminpb.RescueEntryImportRequest) (*emptypb.Empty, error) {
// First make sure an entry with the given name exists.
entry, err := base.Q[mothership_db.Entry](s.db).F("name", req.Name).GetOrNil()
if err != nil {
base.LogErrorf("failed to get entry: %v", err)
return nil, status.Error(codes.Internal, "failed to get entry")
}
if entry == nil {
return nil, status.Error(codes.NotFound, "entry not found")
}
// Make sure the entry is on hold.
if entry.State != mothershippb.Entry_ON_HOLD {
return nil, status.Error(codes.FailedPrecondition, "entry is not on hold")
}
// If on hold, then signal the workflow to continue.
err = s.temporal.SignalWorkflow(ctx, "operations/"+entry.Sha256Sum, "", "rescue", true)
if err != nil {
if strings.Contains(err.Error(), "already completed") {
// For some reason the entry got stuck in a weird state.
// Let's just set the state to FAILED.
entry.State = mothershippb.Entry_FAILED
err = base.Q[mothership_db.Entry](s.db).U(entry)
if err != nil {
base.LogErrorf("failed to update entry: %v", err)
return nil, status.Error(codes.Internal, "failed to update entry")
}
return &emptypb.Empty{}, nil
}
base.LogErrorf("failed to signal workflow: %v", err)
return nil, status.Error(codes.Internal, "failed to signal workflow")
}
return &emptypb.Empty{}, nil
}

View File

@ -1,53 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
"context"
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothership_worker_server "go.resf.org/peridot/tools/mothership/worker_server"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/sdk/client"
"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"strings"
)
func (s *Server) RetractEntry(ctx context.Context, req *mshipadminpb.RetractEntryRequest) (*longrunning.Operation, error) {
startWorkflowOpts := client.StartWorkflowOptions{
ID: "operations/retract/" + req.Name,
WorkflowExecutionErrorWhenAlreadyStarted: true,
WorkflowIDReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY,
}
// Submit to Temporal
run, err := s.temporal.ExecuteWorkflow(
context.Background(),
startWorkflowOpts,
mothership_worker_server.RetractEntryWorkflow,
req.Name,
)
if err != nil {
if strings.Contains(err.Error(), "is already running") {
return nil, status.Error(codes.AlreadyExists, "entry is already running")
}
base.LogErrorf("failed to start workflow: %v", err)
return nil, status.Error(codes.Internal, "failed to start workflow")
}
return s.getOperation(ctx, run.GetID())
}

View File

@ -1,67 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
"go.temporal.io/sdk/client"
"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc"
)
type Server struct {
base.GRPCServer
mshipadminpb.UnimplementedMshipAdminServer
longrunning.UnimplementedOperationsServer
db *base.DB
temporal client.Client
}
func NewServer(db *base.DB, temporalClient client.Client, oidcInterceptorDetails *base.OidcInterceptorDetails, opts ...base.GRPCServerOption) (*Server, error) {
oidcInterceptor, err := base.OidcGrpcInterceptor(oidcInterceptorDetails)
if err != nil {
return nil, err
}
opts = append(opts, base.WithUnaryInterceptors(oidcInterceptor))
grpcServer, err := base.NewGRPCServer(opts...)
if err != nil {
return nil, err
}
return &Server{
GRPCServer: *grpcServer,
db: db,
temporal: temporalClient,
}, nil
}
func (s *Server) Start() error {
s.RegisterService(func(server *grpc.Server) {
longrunning.RegisterOperationsServer(server, s)
mshipadminpb.RegisterMshipAdminServer(server, s)
})
if err := s.GatewayEndpoints(
longrunning.RegisterOperationsHandler,
mshipadminpb.RegisterMshipAdminHandler,
); err != nil {
return err
}
return s.GRPCServer.Start()
}

View File

@ -1,114 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
"context"
"go.ciq.dev/pika"
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothership_db "go.resf.org/peridot/tools/mothership/db"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"strings"
)
func (s *Server) GetWorker(_ context.Context, req *mshipadminpb.GetWorkerRequest) (*mshipadminpb.Worker, error) {
worker, err := base.Q[mothership_db.Worker](s.db).F("name", req.Name).GetOrNil()
if err != nil {
base.LogErrorf("failed to get worker: %v", err)
return nil, status.Error(codes.Internal, "failed to get worker")
}
if worker == nil {
return nil, status.Error(codes.NotFound, "worker not found")
}
return worker.ToPB(), nil
}
func (s *Server) ListWorkers(_ context.Context, req *mshipadminpb.ListWorkersRequest) (*mshipadminpb.ListWorkersResponse, error) {
aipOptions := pika.ProtoReflect(&mshipadminpb.Worker{})
page, nt, err := base.Q[mothership_db.Worker](s.db).GetPage(req, aipOptions)
if err != nil {
base.LogErrorf("failed to get worker page: %v", err)
return nil, status.Error(codes.Internal, "failed to get worker page")
}
return &mshipadminpb.ListWorkersResponse{
Workers: base.SliceToPB[*mshipadminpb.Worker, *mothership_db.Worker](page),
NextPageToken: nt,
}, nil
}
func (s *Server) CreateWorker(_ context.Context, req *mshipadminpb.CreateWorkerRequest) (*mshipadminpb.Worker, error) {
// Verify that the id is at least 4 characters long.
if len(req.WorkerId) < 4 {
return nil, status.Error(codes.InvalidArgument, "worker id must be at least 4 characters long")
}
// Create the worker.
name := base.NameGen("workers")
worker := &mothership_db.Worker{
Name: name,
WorkerID: req.WorkerId,
ApiSecret: base.NameGen(name),
}
err := base.Q[mothership_db.Worker](s.db).Create(worker)
if err != nil {
// if the error is a duplicate key error, return an already exists error.
if strings.Contains(err.Error(), "duplicate key") {
st, _ := status.
New(codes.AlreadyExists, "worker already exists").
WithDetails(&errdetails.LocalizedMessage{
Locale: "en-US",
Message: "Worker with given ID already exists.",
})
return nil, st.Err()
}
base.LogErrorf("failed to create worker: %v", err)
return nil, status.Error(codes.Internal, "failed to create worker")
}
pb := worker.ToPB()
pb.ApiSecret = worker.ApiSecret
return pb, nil
}
func (s *Server) DeleteWorker(_ context.Context, req *mshipadminpb.DeleteWorkerRequest) (*emptypb.Empty, error) {
worker, err := base.Q[mothership_db.Worker](s.db).F("name", req.Name).GetOrNil()
if err != nil {
base.LogErrorf("failed to get worker: %v", err)
return nil, status.Error(codes.Internal, "failed to get worker")
}
if worker == nil {
return nil, status.Error(codes.NotFound, "worker not found")
}
err = base.Q[mothership_db.Worker](s.db).D(worker)
if err != nil {
base.LogErrorf("failed to delete worker: %v", err)
return nil, status.Error(codes.Internal, "failed to delete worker")
}
return &emptypb.Empty{}, nil
}

View File

@ -1,144 +0,0 @@
// Copyright 2023 Peridot Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mothershipadmin_rpc
import (
"github.com/stretchr/testify/require"
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothership_db "go.resf.org/peridot/tools/mothership/db"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"testing"
)
func TestGetWorker_Empty(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
worker, err := s.GetWorker(testContext(), &mshipadminpb.GetWorkerRequest{})
require.NotNil(t, err)
require.Nil(t, worker)
expectedErr := status.Error(codes.NotFound, "worker not found")
require.Equal(t, expectedErr.Error(), err.Error())
}
func TestGetWorker_One(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
require.Nil(t, base.Q[mothership_db.Worker](s.db).Create(&mothership_db.Worker{
Name: "test",
WorkerID: "test-id",
ApiSecret: "secret",
}))
defer func() {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}()
worker, err := s.GetWorker(testContext(), &mshipadminpb.GetWorkerRequest{
Name: "test",
})
require.Nil(t, err)
require.Equal(t, "test", worker.Name)
require.Equal(t, "test-id", worker.WorkerId)
require.Empty(t, worker.ApiSecret)
}
func TestListWorkers_Empty(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
workers, err := s.ListWorkers(testContext(), &mshipadminpb.ListWorkersRequest{})
require.Nil(t, err)
require.Empty(t, workers.Workers)
}
func TestListWorkers_One(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
require.Nil(t, base.Q[mothership_db.Worker](s.db).Create(&mothership_db.Worker{
Name: "test",
WorkerID: "test-id",
ApiSecret: "secret",
}))
defer func() {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}()
workers, err := s.ListWorkers(testContext(), &mshipadminpb.ListWorkersRequest{})
require.Nil(t, err)
require.Len(t, workers.Workers, 1)
require.Equal(t, "test", workers.Workers[0].Name)
require.Equal(t, "test-id", workers.Workers[0].WorkerId)
require.Empty(t, workers.Workers[0].ApiSecret)
}
func TestCreateWorker(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
worker, err := s.CreateWorker(testContext(), &mshipadminpb.CreateWorkerRequest{
WorkerId: "test-id",
})
require.Nil(t, err)
require.Equal(t, "test-id", worker.WorkerId)
require.NotEmpty(t, worker.Name)
require.NotEmpty(t, worker.ApiSecret)
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}
func TestCreateWorker_Duplicate(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
_, err := s.CreateWorker(testContext(), &mshipadminpb.CreateWorkerRequest{
WorkerId: "test-id",
})
require.Nil(t, err)
_, err = s.CreateWorker(testContext(), &mshipadminpb.CreateWorkerRequest{
WorkerId: "test-id",
})
require.NotNil(t, err)
require.Equal(t, codes.AlreadyExists.String(), status.Code(err).String())
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}
func TestCreateWorker_ShortID(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
_, err := s.CreateWorker(testContext(), &mshipadminpb.CreateWorkerRequest{
WorkerId: "id",
})
require.NotNil(t, err)
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}
func TestDeleteWorker(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
worker, err := s.CreateWorker(testContext(), &mshipadminpb.CreateWorkerRequest{
WorkerId: "test-id",
})
require.Nil(t, err)
_, err = s.DeleteWorker(testContext(), &mshipadminpb.DeleteWorkerRequest{
Name: worker.Name,
})
require.Nil(t, err)
_, err = s.GetWorker(testContext(), &mshipadminpb.GetWorkerRequest{
Name: worker.Name,
})
require.NotNil(t, err)
require.Equal(t, codes.NotFound.String(), status.Code(err).String())
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}
func TestDeleteWorker_NotFound(t *testing.T) {
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
_, err := s.DeleteWorker(testContext(), &mshipadminpb.DeleteWorkerRequest{
Name: "test",
})
require.NotNil(t, err)
require.Equal(t, codes.NotFound.String(), status.Code(err).String())
require.Nil(t, base.Q[mothership_db.Worker](s.db).Delete())
}

View File

@ -1,88 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { Theme } from '@mui/material/styles';
import EngineeringIcon from '@mui/icons-material/Engineering';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import { Drawer } from 'base/ts/mui/Drawer';
import { CreateWorker } from 'tools/mothership/admin/ui/CreateWorker';
import { GetWorker } from 'tools/mothership/admin/ui/GetWorker';
import { Entries } from './Entries';
import { GetEntry } from './GetEntry';
import { Workers } from './Workers';
export const App = () => {
return (
<Box sx={{ display: 'flex' }}>
<AppBar
elevation={5}
position="fixed"
sx={{ zIndex: (theme: Theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar variant="dense">
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Mship Admin{window.__beta__ ? ' (beta)' : ''}
</Typography>
<Box sx={{ flexGrow: 1, textAlign: 'right' }}>
<Button className="native-link" href="/admin/auth/oidc/logout" variant="primary">
Logout
</Button>
<Button className="native-link" href="/" variant="primary">
Go back to Mship
</Button>
</Box>
</Toolbar>
</AppBar>
<Drawer
sections={[
{
links: [
{ text: 'Workers', href: '/workers', icon: <EngineeringIcon /> },
{ text: 'Entries', href: '/entries', icon: <ImportExportIcon /> },
],
},
]}
/>
<Box component="main" sx={{ flexGrow: 1 }}>
<Toolbar variant="dense" />
<Routes>
<Route index element={<Navigate to="/workers" replace />} />
<Route path="/workers">
<Route index element={<Workers />} />
<Route path="create" element={<CreateWorker />} />
<Route path=":name" element={<GetWorker />} />
</Route>
<Route path="/entries">
<Route index element={<Entries />} />
<Route path=":name" element={<GetEntry />} />
</Route>
</Routes>
</Box>
</Box>
);
};

View File

@ -1,38 +0,0 @@
# Copyright 2023 Peridot Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//tools/build_rules/ui_bundle:defs.bzl", "ui_bundle")
ui_bundle(
name = "bundle",
deps = [
"//:node_modules/@mui/icons-material",
"//:node_modules/@mui/material",
"//base/ts/mui",
"//tools/mothership/proto/admin/v1:mshipadminpb_ts_proto",
],
)
go_library(
name = "ui",
srcs = ["ui.go"],
# keep
embedsrcs = [
":bundle", # keep
],
importpath = "go.resf.org/peridot/tools/mothership/admin/ui",
visibility = ["//visibility:public"],
deps = ["//base/go"],
)

View File

@ -1,70 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import { NewResource } from 'base/ts/mui/NewResource';
import { reqap } from 'base/ts/reqap';
import { V1Worker } from 'bazel-bin/tools/mothership/proto/admin/v1/mshipadminpb_ts_proto_gen';
import { mshipAdminApi } from 'tools/mothership/admin/ui/api';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
export const CreateWorker = () => {
return (
<Box>
<Box sx={{ p: 1.5 }}>Create a new worker</Box>
<Divider />
<Box sx={{ p: 1.5, mt: 2 }}>
<NewResource
fields={[
{
key: 'workerId',
label: 'Worker ID',
subtitle:
'This ID will be used to uniquely identify this worker.',
type: 'text',
required: true,
},
]}
save={(x: V1Worker) =>
reqap(
mshipAdminApi.createWorker({ body: { workerId: x.workerId!! } }),
)
}
showDialog={(resource: V1Worker) => (
<>
<DialogTitle>Worker created</DialogTitle>
<DialogContent>
<DialogContentText>
The worker API secret is <code>{resource.apiSecret}</code>.
<br /><br />
This is the only time it will be shown, so make sure to save
it somewhere safe.
</DialogContentText>
</DialogContent>
</>
)}
/>
</Box>
</Box>
);
};

View File

@ -1,46 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import Box from '@mui/material/Box';
import { ResourceTable } from 'base/ts/mui/ResourceTable';
import { srpmArchiverApi } from 'tools/mothership/admin/ui/api';
import {
V1ListEntriesResponse,
V1Entry,
} from 'bazel-bin/tools/mothership/proto/v1/mothershippb_ts_proto_gen';
import { reqap } from 'base/ts/reqap';
export const Entries = () => {
return (
<Box sx={{p: 1.5, px: 3, width: '100%'}}>
<ResourceTable<V1Entry>
load={(pageSize: number, pageToken?: string) => reqap(srpmArchiverApi.listEntries({
pageSize: pageSize,
pageToken: pageToken,
}))}
transform={((response: V1ListEntriesResponse) => response.entries || [])}
fields={[
{ key: 'name', label: 'Entry Name' },
{ key: 'entryId', label: 'Entry ID' },
{ key: 'state', label: 'State' },
]}
/>
</Box>
);
};

View File

@ -1,142 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useParams } from 'react-router-dom';
import { ResourceView } from 'base/ts/mui/ResourceView';
import { reqap } from 'base/ts/reqap';
import {
EntryState,
V1Entry,
} from 'bazel-bin/tools/mothership/proto/v1/mothershippb_ts_proto_gen';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import { mshipAdminApi, srpmArchiverApi } from 'tools/mothership/admin/ui/api';
import Button from '@mui/material/Button';
export const GetEntry = () => {
const params = useParams();
const [resource, setResource] = React.useState<V1Entry | undefined | null>(
undefined,
);
// Load the resource
React.useEffect(() => {
(async () => {
const [res, err] = await reqap(
srpmArchiverApi.getEntry({
name1: `entries/${params.name}`,
}),
);
if (err) {
setResource(null);
return;
}
setResource(res);
})().then();
}, []);
// Rescue the entry (call API)
const rescueEntry = async () => {
const [res, err] = await reqap(
mshipAdminApi.rescueEntryImport({
name: `entries/${params.name}`,
}),
);
if (err) {
return;
}
window.location.reload();
};
// Retract the entry (call API)
const retractEntry = async () => {
const [res, err] = await reqap(
mshipAdminApi.retractEntry({
name: `entries/${params.name}`,
}),
);
if (err) {
return;
}
window.location.reload();
};
return (
<Box>
<Box
sx={{
px: 1.5,
height: '48px',
display: 'flex',
justifyContent: 'justify-between',
alignItems: 'center',
}}
>
<span>entries/{params.name}</span>
{resource && resource.state == EntryState.OnHold && (
<Button
sx={{ ml: 'auto', textAlign: 'right' }}
variant="outlined"
onClick={rescueEntry}
>
Rescue
</Button>
)}
{resource && resource.state == EntryState.Archived && (
<Button
sx={{ ml: 'auto', textAlign: 'right' }}
variant="outlined"
color="error"
onClick={retractEntry}
>
Retract
</Button>
)}
</Box>
<Divider />
<Box sx={{ p: 1.5 }}>
<ResourceView
resource={resource}
fields={[
{ key: 'entryId', label: 'Entry ID' },
{ key: 'createTime', label: 'Created' },
{ key: 'osRelease', label: 'OS Release' },
{ key: 'sha256Sum', label: 'SHA256 Sum' },
{ key: 'repository', label: 'Repository' },
{ key: 'workerId', label: 'Worker ID' },
{ key: 'commitUri', label: 'Commit URI', linkToSelf: true },
{ key: 'commitHash', label: 'Commit Hash' },
{ key: 'state', label: 'State' },
]}
/>
</Box>
{resource && resource.state == EntryState.OnHold && (
<Box sx={{ p: 1.5 }}>
<code>{resource.errorMessage}</code>
</Box>
)}
</Box>
);
};

View File

@ -1,121 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ResourceView } from 'base/ts/mui/ResourceView';
import { reqap } from 'base/ts/reqap';
import { mshipAdminApi } from 'tools/mothership/admin/ui/api';
import { V1Worker } from 'bazel-bin/tools/mothership/proto/admin/v1/mshipadminpb_ts_proto_gen';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContentText from '@mui/material/DialogContentText';
import DialogContent from '@mui/material/DialogContent';
export const GetWorker = () => {
const navigate = useNavigate();
const params = useParams();
const [resource, setResource] = React.useState<V1Worker | undefined | null>(
undefined,
);
const [deleteOpen, setDeleteOpen] = React.useState<boolean>(false);
// Load the resource
React.useEffect(() => {
(async () => {
const [res, err] = await reqap(
mshipAdminApi.getWorker({
name: `workers/${params.name}`,
}),
);
if (err) {
setResource(null);
return;
}
setResource(res);
})().then();
}, []);
const doDelete = async () => {
const [res, err] = await reqap(
mshipAdminApi.deleteWorker({
name: `workers/${params.name}`,
}),
);
if (err) {
setDeleteOpen(false);
return;
}
navigate('/');
};
return (
<Box>
<Dialog open={deleteOpen}>
<DialogContent>
<DialogContentText>
Are you sure you want to delete this worker?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
<Button onClick={doDelete} color="error">Delete</Button>
</DialogActions>
</Dialog>
<Box
sx={{
px: 1.5,
height: '48px',
display: 'flex',
justifyContent: 'justify-between',
alignItems: 'center',
}}
>
<span>workers/{params.name}</span>
<Button
sx={{ ml: 'auto', textAlign: 'right' }}
variant="outlined"
color="error"
onClick={() => setDeleteOpen(true)}
>
Delete
</Button>
</Box>
<Divider />
<Box sx={{ p: 1.5 }}>
<ResourceView
resource={resource}
fields={[
{ key: 'workerId', label: 'Worker ID' },
{ key: 'createTime', label: 'Created' },
{ key: 'lastCheckinTime', label: 'Last active' },
]}
/>
</Box>
</Box>
);
};

View File

@ -1,56 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { ResourceTable } from 'base/ts/mui/ResourceTable';
import { reqap } from 'base/ts/reqap';
import { mshipAdminApi } from 'tools/mothership/admin/ui/api';
import {
V1ListWorkersResponse,
V1Worker,
} from 'bazel-bin/tools/mothership/proto/admin/v1/mshipadminpb_ts_proto_gen';
export const Workers = () => {
return (
<Box sx={{p: 1.5, px: 3, width: '100%'}}>
<Box sx={{ mb: 2, ml: 'auto', textAlign: 'right' }}>
<Button href="/workers/create" variant="outlined" size="small">
Create a new worker
</Button>
</Box>
<ResourceTable<V1Worker>
load={(pageSize: number, pageToken?: string) =>
reqap(
mshipAdminApi.listWorkers({
pageSize: pageSize,
pageToken: pageToken,
}),
)
}
transform={(response: V1ListWorkersResponse) => response.workers || []}
fields={[
{ key: 'name', label: 'Worker Name' },
{ key: 'workerId', label: 'Worker ID' },
{ key: 'createTime', label: 'Created' },
]}
/>
</Box>
);
};

View File

@ -1,30 +0,0 @@
/**
* Copyright 2023 Peridot Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as mshipAdmin from 'bazel-bin/tools/mothership/proto/admin/v1/mshipadminpb_ts_proto_gen';
import * as srpmArchiver from 'bazel-bin/tools/mothership/proto/v1/mothershippb_ts_proto_gen';
const archiverCfg = new srpmArchiver.Configuration({
basePath: '/api',
})
export const srpmArchiverApi = new srpmArchiver.SrpmArchiverApi(archiverCfg);
const cfg = new mshipAdmin.Configuration({
basePath: '/admin/api',
})
export const mshipAdminApi = new mshipAdmin.MshipAdminApi(cfg);

View File

@ -1,34 +0,0 @@
/**
* Copyright 2023 Peridot Authors