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
*
* 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 { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import CssBaseline from '@mui/material/CssBaseline';
import ThemeProvider from '@mui/material/styles/ThemeProvider';
import { App } from './App';
import { peridotDarkTheme } from 'base/ts/mui/theme';
const root = createRoot(document.getElementById('app') || document.body);
root.render(
<BrowserRouter basename={window.__peridot_prefix__ || ''}>
<ThemeProvider theme={peridotDarkTheme}>
<CssBaseline />
<App />
</ThemeProvider>
</BrowserRouter>
);

View File

@ -1,32 +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 mship_admin_ui
import (
"embed"
base "go.resf.org/peridot/base/go"
)
//go:embed *
var assets embed.FS
func InitFrontendInfo(info *base.FrontendInfo) *embed.FS {
if info == nil {
info = &base.FrontendInfo{}
}
info.Title = "Mship Admin"
return &assets
}

View File

@ -1,34 +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_binary", "go_library")
go_library(
name = "mship_admin_server_lib",
srcs = ["main.go"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_admin_server",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//tools/mothership/admin/rpc",
"//vendor/github.com/urfave/cli/v2:cli",
"//vendor/go.temporal.io/sdk/client",
],
)
go_binary(
name = "mship_admin_server",
embed = [":mship_admin_server_lib"],
visibility = ["//visibility:public"],
)

View File

@ -1,64 +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 main
import (
"github.com/urfave/cli/v2"
base "go.resf.org/peridot/base/go"
mothershipadmin_rpc "go.resf.org/peridot/tools/mothership/admin/rpc"
"go.temporal.io/sdk/client"
"os"
)
func run(ctx *cli.Context) error {
oidcInterceptorDetails, err := base.FlagsToOidcInterceptorDetails(ctx)
if err != nil {
return err
}
temporalClient, err := base.GetTemporalClientFromFlags(ctx, client.Options{})
if err != nil {
return err
}
s, err := mothershipadmin_rpc.NewServer(
base.GetDBFromFlags(ctx),
temporalClient,
oidcInterceptorDetails,
base.FlagsToGRPCServerOptions(ctx)...,
)
if err != nil {
return err
}
return s.Start()
}
func main() {
app := &cli.App{
Name: "mship_admin_server",
Action: run,
Flags: base.WithFlags(
base.WithDatabaseFlags("mothership"),
base.WithTemporalFlags("", "mship_worker_server"),
base.WithGrpcFlags(6687),
base.WithGatewayFlags(6688),
base.WithOidcFlags("", "releng"),
),
}
if err := app.Run(os.Args); err != nil {
base.LogFatalf("failed to start mship_admin_server: %v", err)
}
}

View File

@ -1,33 +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_binary", "go_library")
go_library(
name = "mship_admin_ui_lib",
srcs = ["main.go"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_admin_ui",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//tools/mothership/admin/ui",
"//vendor/github.com/urfave/cli/v2:cli",
],
)
go_binary(
name = "mship_admin_ui",
embed = [":mship_admin_ui_lib"],
visibility = ["//visibility:public"],
)

View File

@ -1,44 +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 main
import (
"github.com/urfave/cli/v2"
base "go.resf.org/peridot/base/go"
mship_admin_ui "go.resf.org/peridot/tools/mothership/admin/ui"
"os"
)
func run(ctx *cli.Context) error {
info := base.FlagsToFrontendInfo(ctx)
assets := mship_admin_ui.InitFrontendInfo(info)
return base.FrontendServer(info, assets)
}
func main() {
app := &cli.App{
Name: "mship_admin_ui",
Action: run,
Flags: base.WithFlags(
base.WithFrontendFlags(9112),
base.WithFrontendAuthFlags(""),
),
}
if err := app.Run(os.Args); err != nil {
base.LogFatalf("failed to start mship_ui: %v", err)
}
}

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_binary", "go_library")
go_library(
name = "mship_dev_lib",
srcs = ["main.go"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_dev",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//tools/mothership/admin/rpc",
"//tools/mothership/admin/ui",
"//tools/mothership/rpc",
"//tools/mothership/ui",
"//vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime",
"//vendor/github.com/urfave/cli/v2:cli",
"//vendor/go.temporal.io/sdk/client",
],
)
go_binary(
name = "mship_dev",
embed = [":mship_dev_lib"],
visibility = ["//visibility:public"],
)

View File

@ -1,182 +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 main implements the dev server for Mothership.
// This runs the services just like it would be structured in production. (The RESF way)
// This means:
// - localhost:9111 serves the external UI
// - localhost:9111/admin serves the admin UI
// - localhost:9111/api serves the external API
// - localhost:9111/admin/api serves the admin API
package main
import (
"fmt"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/urfave/cli/v2"
base "go.resf.org/peridot/base/go"
mothershipadmin_rpc "go.resf.org/peridot/tools/mothership/admin/rpc"
mship_admin_ui "go.resf.org/peridot/tools/mothership/admin/ui"
mothership_rpc "go.resf.org/peridot/tools/mothership/rpc"
mship_ui "go.resf.org/peridot/tools/mothership/ui"
"go.temporal.io/sdk/client"
"net/http"
"os"
"strings"
)
var (
apiGrpcPort = 3334
adminApiGrpcPort = 3335
)
func setupUi(ctx *cli.Context) error {
info := base.FlagsToFrontendInfo(ctx)
assets := mship_ui.InitFrontendInfo(info)
info.NoRun = true
info.Self = "http://localhost:9111"
return base.FrontendServer(info, assets)
}
func setupAdminUi(ctx *cli.Context) (*base.FrontendInfo, error) {
info := base.FlagsToFrontendInfo(ctx)
assets := mship_admin_ui.InitFrontendInfo(info)
info.NoRun = true
info.Self = "http://localhost:9111/admin"
info.OIDCGroup = "authors"
err := base.FrontendServer(info, assets)
if err != nil {
return nil, err
}
return info, nil
}
func setupApi(ctx *cli.Context) (*runtime.ServeMux, error) {
temporalClient, err := base.GetTemporalClientFromFlags(ctx, client.Options{})
if err != nil {
return nil, err
}
s, err := mothership_rpc.NewServer(
base.GetDBFromFlags(ctx),
temporalClient,
base.WithGRPCPort(apiGrpcPort),
base.WithNoGRPCGateway(),
base.WithNoMetrics(),
)
if err != nil {
return nil, err
}
go func() {
err := s.Start()
if err != nil {
base.LogFatalf("failed to start mship_api: %v", err)
}
}()
return s.GatewayMux(), nil
}
func setupAdminApi(ctx *cli.Context) (*runtime.ServeMux, error) {
oidcInterceptorDetails, err := base.FlagsToOidcInterceptorDetails(ctx)
if err != nil {
return nil, err
}
oidcInterceptorDetails.Group = "authors"
temporalClient, err := base.GetTemporalClientFromFlags(ctx, client.Options{})
if err != nil {
return nil, err
}
s, err := mothershipadmin_rpc.NewServer(
base.GetDBFromFlags(ctx),
temporalClient,
oidcInterceptorDetails,
base.WithGRPCPort(adminApiGrpcPort),
base.WithNoGRPCGateway(),
base.WithNoMetrics(),
)
if err != nil {
return nil, err
}
go func() {
err := s.Start()
if err != nil {
base.LogFatalf("failed to start mship_admin_api: %v", err)
}
}()
return s.GatewayMux(), nil
}
func run(ctx *cli.Context) error {
err := setupUi(ctx)
if err != nil {
return err
}
adminInfo, err := setupAdminUi(ctx)
if err != nil {
return err
}
apiMux, err := setupApi(ctx)
if err != nil {
return err
}
http.HandleFunc("/api/", http.StripPrefix("/api", apiMux).ServeHTTP)
adminApiMux, err := setupAdminApi(ctx)
if err != nil {
return err
}
http.HandleFunc("/admin/api/", http.StripPrefix("/admin/api", adminApiMux).ServeHTTP)
handler := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Set("x-peridot-beta", "true")
if strings.HasPrefix(r.URL.Path, "/admin") {
adminInfo.MuxHandler.ServeHTTP(w, r)
} else {
h.ServeHTTP(w, r)
}
})
}
// Start server
port := 9111
base.LogInfof("Starting server on port %d", port)
return http.ListenAndServe(fmt.Sprintf(":%d", port), handler(http.DefaultServeMux))
}
func main() {
app := &cli.App{
Name: "mship_dev",
Action: run,
Flags: base.WithFlags(
base.WithDatabaseFlags("mothership"),
base.WithTemporalFlags("", "mship_worker_server"),
base.WithFrontendAuthFlags(""),
),
}
if err := app.Run(os.Args); err != nil {
base.LogFatalf("failed to start mship_dev: %v", err)
}
}

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.
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "mship_api_lib",
srcs = ["main.go"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_api",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//tools/mothership/rpc",
"//vendor/github.com/urfave/cli/v2:cli",
],
)
go_binary(
name = "mship_api",
embed = [":mship_server_lib"],
visibility = ["//visibility:public"],
)
go_library(
name = "mship_server_lib",
srcs = ["main.go"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_server",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//tools/mothership/rpc",
"//vendor/github.com/urfave/cli/v2:cli",
"//vendor/go.temporal.io/sdk/client",
],
)

View File

@ -1,22 +0,0 @@
exec("base/ci/defaults.star")
namespace("mship")
service(
name = "mship_api",
image = "%s/mship_api:%s" % (args().registry, args().version),
ports = [
Port(
name = "grpc",
port = 6677,
),
Port(
name = "http",
port = 6678,
expose = True,
external = True,
host = "mship.resf.org",
path = "/api",
),
],
)

View File

@ -1,57 +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 main
import (
"github.com/urfave/cli/v2"
base "go.resf.org/peridot/base/go"
mothership_rpc "go.resf.org/peridot/tools/mothership/rpc"
"go.temporal.io/sdk/client"
"os"
)
func run(ctx *cli.Context) error {
temporalClient, err := base.GetTemporalClientFromFlags(ctx, client.Options{})
if err != nil {
return err
}
s, err := mothership_rpc.NewServer(
base.GetDBFromFlags(ctx),
temporalClient,
base.FlagsToGRPCServerOptions(ctx)...,
)
if err != nil {
return err
}
return s.Start()
}
func main() {
app := &cli.App{
Name: "mship_server",
Action: run,
Flags: base.WithFlags(
base.WithDatabaseFlags("mothership"),
base.WithTemporalFlags("", "mship_worker_server"),
base.WithGrpcFlags(6677),
base.WithGatewayFlags(6678),
),
}
if err := app.Run(os.Args); err != nil {
base.LogFatalf("failed to start mship_api: %v", err)
}
}

View File

@ -1,33 +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_binary", "go_library")
go_library(
name = "mship_ui_lib",
srcs = ["main.go"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_ui",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//tools/mothership/ui",
"//vendor/github.com/urfave/cli/v2:cli",
],
)
go_binary(
name = "mship_ui",
embed = [":mship_ui_lib"],
visibility = ["//visibility:public"],
)

View File

@ -1,41 +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 main
import (
"github.com/urfave/cli/v2"
base "go.resf.org/peridot/base/go"
mship_ui "go.resf.org/peridot/tools/mothership/ui"
"os"
)
func run(ctx *cli.Context) error {
info := base.FlagsToFrontendInfo(ctx)
assets := mship_ui.InitFrontendInfo(info)
return base.FrontendServer(info, assets)
}
func main() {
app := &cli.App{
Name: "mship_ui",
Action: run,
Flags: base.WithFrontendFlags(9111),
}
if err := app.Run(os.Args); err != nil {
base.LogFatalf("failed to start mship_ui: %v", err)
}
}

View File

@ -1,40 +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_binary", "go_library")
go_library(
name = "mship_worker_server_lib",
srcs = ["main.go"],
embedsrcs = ["rh_public_key.asc"],
importpath = "go.resf.org/peridot/tools/mothership/cmd/mship_worker_server",
visibility = ["//visibility:private"],
deps = [
"//base/go",
"//base/go/forge",
"//base/go/forge/github",
"//base/go/storage/detector",
"//tools/mothership/worker_server",
"//vendor/github.com/urfave/cli/v2:cli",
"//vendor/go.temporal.io/sdk/client",
"//vendor/go.temporal.io/sdk/worker",
"//vendor/golang.org/x/crypto/openpgp",
],
)
go_binary(
name = "mship_worker_server",
embed = [":mship_worker_server_lib"],
visibility = ["//visibility:public"],
)

View File

@ -1,208 +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 main
import (
"bytes"
_ "embed"
"encoding/base64"
"github.com/urfave/cli/v2"
base "go.resf.org/peridot/base/go"
"go.resf.org/peridot/base/go/forge"
github_forge "go.resf.org/peridot/base/go/forge/github"
storage_detector "go.resf.org/peridot/base/go/storage/detector"
mothership_worker_server "go.resf.org/peridot/tools/mothership/worker_server"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"golang.org/x/crypto/openpgp"
"os"
)
//go:embed rh_public_key.asc
var defaultGpgKey []byte
func run(ctx *cli.Context) error {
temporalClient, err := base.GetTemporalClientFromFlags(ctx, client.Options{})
if err != nil {
return err
}
db := base.GetDBFromFlags(ctx)
storage, err := storage_detector.FromFlags(ctx)
if err != nil {
return err
}
// Create pgp keys
var gpgKeys openpgp.EntityList
for _, key := range ctx.StringSlice("allowed-gpg-keys") {
decoded, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return err
}
keyRing, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(decoded))
if err != nil {
return err
}
gpgKeys = append(gpgKeys, keyRing...)
}
// Create forge based on git provider
var remoteForge forge.Forge
switch ctx.String("git-provider") {
case "github":
var appPrivateKey []byte
if ctx.Bool("github-app-private-key-base64") {
appPrivateKey, err = base64.StdEncoding.DecodeString(ctx.String("github-app-private-key"))
if err != nil {
return err
}
} else {
appPrivateKey = []byte(ctx.String("github-app-private-key"))
}
remoteForge, err = github_forge.New(
ctx.String("github-org"),
ctx.String("github-app-id"),
appPrivateKey,
ctx.Bool("github-make-repo-public"),
)
if err != nil {
return err
}
remoteForge = forge.NewCacher(remoteForge)
default:
return cli.Exit("git-provider must be github", 1)
}
w := worker.New(temporalClient, ctx.String("temporal-task-queue"), worker.Options{})
workerServer := mothership_worker_server.New(
db,
storage,
gpgKeys,
remoteForge,
ctx.Bool("import-rolling-release"),
)
// Register workflows
w.RegisterWorkflow(mothership_worker_server.ProcessRPMWorkflow)
w.RegisterWorkflow(mothership_worker_server.RetractEntryWorkflow)
// Register activities
w.RegisterActivity(workerServer)
// Start worker
return w.Run(worker.InterruptCh())
}
func main() {
flags := base.WithFlags(
base.WithDatabaseFlags("mothership"),
base.WithTemporalFlags("", "mship_worker_server"),
base.WithStorageFlags(),
[]cli.Flag{
&cli.StringSliceFlag{
Name: "allowed-gpg-keys",
Usage: "Armored GPG keys that we verify SRPMs with. Must be base64 encoded",
EnvVars: []string{"ALLOWED_GPG_KEYS"},
},
&cli.BoolFlag{
Name: "import-rolling-release",
Usage: "Whether to import packages in rolling release mode",
EnvVars: []string{"IMPORT_ROLLING_RELEASE"},
Value: false,
},
&cli.StringFlag{
Name: "git-provider",
Action: func(ctx *cli.Context, s string) error {
// Can only be github for now
if s != "github" {
return cli.Exit("git-provider must be github", 1)
}
return nil
},
Usage: "Git provider to use. Currently only github is supported",
EnvVars: []string{"GIT_PROVIDER"},
},
// Github only
&cli.StringFlag{
Name: "github-org",
Usage: "Github organization to use",
EnvVars: []string{"GITHUB_ORG"},
Action: func(ctx *cli.Context, s string) error {
// Required for github
if ctx.String("git-provider") == "github" && s == "" {
return cli.Exit("github-org is required for github", 1)
}
return nil
},
},
&cli.StringFlag{
Name: "github-app-id",
Usage: "Github app ID",
EnvVars: []string{"GITHUB_APP_ID"},
Action: func(ctx *cli.Context, s string) error {
// Required for github
if ctx.String("git-provider") == "github" && s == "" {
return cli.Exit("github-org is required for github", 1)
}
return nil
},
},
&cli.StringFlag{
Name: "github-app-private-key",
Usage: "Github app private key",
EnvVars: []string{"GITHUB_APP_PRIVATE_KEY"},
Action: func(ctx *cli.Context, s string) error {
// Required for github
if ctx.String("git-provider") == "github" && s == "" {
return cli.Exit("github-org is required for github", 1)
}
return nil
},
},
&cli.BoolFlag{
Name: "github-app-private-key-base64",
Usage: "Whether the Github app private key is base64 encoded",
EnvVars: []string{"GITHUB_APP_PRIVATE_KEY_BASE64"},
Value: false,
},
&cli.BoolFlag{
Name: "github-make-repo-public",
Usage: "Whether to make the Github repository public",
EnvVars: []string{"GITHUB_MAKE_REPO_PUBLIC"},
Value: false,
},
},
)
base64EncodedDefaultGpgKey := base64.StdEncoding.EncodeToString(defaultGpgKey)
base.RareUseChangeDefault("ALLOWED_GPG_KEYS", base64EncodedDefaultGpgKey)
app := &cli.App{
Name: "mship_worker_server",
Action: run,
Flags: flags,
}
if err := app.Run(os.Args); err != nil {
base.LogFatalf("failed to run mship_worker_server: %v", err)
}
}

View File

@ -1,29 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.5 (GNU/Linux)
mQINBErgSTsBEACh2A4b0O9t+vzC9VrVtL1AKvUWi9OPCjkvR7Xd8DtJxeeMZ5eF
0HtzIG58qDRybwUe89FZprB1ffuUKzdE+HcL3FbNWSSOXVjZIersdXyH3NvnLLLF
0DNRB2ix3bXG9Rh/RXpFsNxDp2CEMdUvbYCzE79K1EnUTVh1L0Of023FtPSZXX0c
u7Pb5DI5lX5YeoXO6RoodrIGYJsVBQWnrWw4xNTconUfNPk0EGZtEnzvH2zyPoJh
XGF+Ncu9XwbalnYde10OCvSWAZ5zTCpoLMTvQjWpbCdWXJzCm6G+/hx9upke546H
5IjtYm4dTIVTnc3wvDiODgBKRzOl9rEOCIgOuGtDxRxcQkjrC+xvg5Vkqn7vBUyW
9pHedOU+PoF3DGOM+dqv+eNKBvh9YF9ugFAQBkcG7viZgvGEMGGUpzNgN7XnS1gj
/DPo9mZESOYnKceve2tIC87p2hqjrxOHuI7fkZYeNIcAoa83rBltFXaBDYhWAKS1
PcXS1/7JzP0ky7d0L6Xbu/If5kqWQpKwUInXtySRkuraVfuK3Bpa+X1XecWi24JY
HVtlNX025xx1ewVzGNCTlWn1skQN2OOoQTV4C8/qFpTW6DTWYurd4+fE0OJFJZQF
buhfXYwmRlVOgN5i77NTIJZJQfYFj38c/Iv5vZBPokO6mffrOTv3MHWVgQARAQAB
tDNSZWQgSGF0LCBJbmMuIChyZWxlYXNlIGtleSAyKSA8c2VjdXJpdHlAcmVkaGF0
LmNvbT6JAjYEEwECACAFAkrgSTsCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAK
CRAZni+R/UMdUWzpD/9s5SFR/ZF3yjY5VLUFLMXIKUztNN3oc45fyLdTI3+UClKC
2tEruzYjqNHhqAEXa2sN1fMrsuKec61Ll2NfvJjkLKDvgVIh7kM7aslNYVOP6BTf
C/JJ7/ufz3UZmyViH/WDl+AYdgk3JqCIO5w5ryrC9IyBzYv2m0HqYbWfphY3uHw5
un3ndLJcu8+BGP5F+ONQEGl+DRH58Il9Jp3HwbRa7dvkPgEhfFR+1hI+Btta2C7E
0/2NKzCxZw7Lx3PBRcU92YKyaEihfy/aQKZCAuyfKiMvsmzs+4poIX7I9NQCJpyE
IGfINoZ7VxqHwRn/d5mw2MZTJjbzSf+Um9YJyA0iEEyD6qjriWQRbuxpQXmlAJbh
8okZ4gbVFv1F8MzK+4R8VvWJ0XxgtikSo72fHjwha7MAjqFnOq6eo6fEC/75g3NL
Ght5VdpGuHk0vbdENHMC8wS99e5qXGNDued3hlTavDMlEAHl34q2H9nakTGRF5Ki
JUfNh3DVRGhg8cMIti21njiRh7gyFI2OccATY7bBSr79JhuNwelHuxLrCFpY7V25
OFktl15jZJaMxuQBqYdBgSay2G0U6D1+7VsWufpzd/Abx1/c3oi9ZaJvW22kAggq
dzdA27UUYjWvx42w9menJwh/0jeQcTecIUd0d0rFcw/c1pvgMMl/Q73yzKgKYw==
=zbHE
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,32 +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_library(
name = "db",
srcs = [
"batch.go",
"entry.go",
"worker.go",
],
importpath = "go.resf.org/peridot/tools/mothership/db",
visibility = ["//visibility:public"],
deps = [
"//base/go",
"//tools/mothership/proto/admin/v1:pb",
"//tools/mothership/proto/v1:pb",
"@org_golang_google_protobuf//types/known/timestamppb",
],
)

View File

@ -1,52 +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 mothership_db
import (
"database/sql"
base "go.resf.org/peridot/base/go"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"google.golang.org/protobuf/types/known/timestamppb"
"time"
)
type Batch struct {
PikaTableName string `pika:"batches"`
PikaDefaultOrderBy string `pika:"-create_time"`
Name string `db:"name"`
BatchID string `db:"batch_id"`
WorkerID string `db:"worker_id"`
CreateTime time.Time `db:"create_time" pika:"omitempty"`
UpdateTime time.Time `db:"create_time" pika:"omitempty"`
SealTime sql.NullTime `db:"create_time"`
BugtrackerURI sql.NullString `db:"bugtracker_uri"`
}
func (b *Batch) GetID() string {
return b.Name
}
func (b *Batch) ToPB() *mothershippb.Batch {
return &mothershippb.Batch{
Name: b.Name,
BatchId: b.BatchID,
WorkerId: b.WorkerID,
CreateTime: timestamppb.New(b.CreateTime),
UpdateTime: timestamppb.New(b.CreateTime),
SealTime: base.SqlNullTime(b.SealTime),
BugtrackerUri: base.SqlNullString(b.BugtrackerURI),
}
}

View File

@ -1,68 +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 mothership_db
import (
"database/sql"
base "go.resf.org/peridot/base/go"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"google.golang.org/protobuf/types/known/timestamppb"
"time"
)
type Entry struct {
PikaTableName string `pika:"entries"`
PikaDefaultOrderBy string `pika:"-create_time"`
Name string `db:"name"`
EntryID string `db:"entry_id"`
CreateTime time.Time `db:"create_time" pika:"omitempty"`
OSRelease string `db:"os_release"`
Sha256Sum string `db:"sha256_sum"`
RepositoryName string `db:"repository_name"`
WorkerID sql.NullString `db:"worker_id"`
BatchName sql.NullString `db:"batch_name"`
UserEmail sql.NullString `db:"user_email"`
CommitURI string `db:"commit_uri"`
CommitHash string `db:"commit_hash"`
CommitBranch string `db:"commit_branch"`
CommitTag string `db:"commit_tag"`
State mothershippb.Entry_State `db:"state"`
PackageName string `db:"package_name"`
}
func (e *Entry) GetID() string {
return e.Name
}
func (e *Entry) ToPB() *mothershippb.Entry {
return &mothershippb.Entry{
Name: e.Name,
EntryId: e.EntryID,
CreateTime: timestamppb.New(e.CreateTime),
OsRelease: e.OSRelease,
Sha256Sum: e.Sha256Sum,
Repository: e.RepositoryName,
WorkerId: base.SqlNullString(e.WorkerID),
Batch: base.SqlNullString(e.BatchName),
UserEmail: base.SqlNullString(e.UserEmail),
CommitUri: e.CommitURI,
CommitHash: e.CommitHash,
CommitBranch: e.CommitBranch,
CommitTag: e.CommitTag,
State: e.State,
Pkg: e.PackageName,
}
}

View File

@ -1,47 +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 mothership_db
import (
"database/sql"
base "go.resf.org/peridot/base/go"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
"google.golang.org/protobuf/types/known/timestamppb"
"time"
)
type Worker struct {
PikaTableName string `pika:"workers"`
PikaDefaultOrderBy string `pika:"-create_time"`
Name string `db:"name"`
CreateTime time.Time `db:"create_time" pika:"omitempty"`
WorkerID string `db:"worker_id"`
LastCheckinTime sql.NullTime `db:"last_checkin_time"`
ApiSecret string `db:"api_secret"`
}
func (w *Worker) GetID() string {
return w.Name
}
func (w *Worker) ToPB() *mshipadminpb.Worker {
return &mshipadminpb.Worker{
Name: w.Name,
WorkerId: w.WorkerID,
CreateTime: timestamppb.New(w.CreateTime),
LastCheckinTime: base.SqlNullTime(w.LastCheckinTime),
}
}

View File

@ -1,14 +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.

View File

@ -1,60 +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.
CREATE TABLE workers
(
name VARCHAR(255) PRIMARY KEY,
worker_id VARCHAR(255) UNIQUE NOT NULL,
create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_checkin_time TIMESTAMPTZ,
api_secret VARCHAR(255) NOT NULL
);
CREATE TABLE entries
(
name VARCHAR(255) PRIMARY KEY,
entry_id VARCHAR(255) NOT NULL,
create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
os_release TEXT NOT NULL,
sha256_sum VARCHAR(255) NOT NULL,
repository_name VARCHAR(255) NOT NULL,
worker_id VARCHAR(255) REFERENCES workers (worker_id),
batch_name VARCHAR(255),
user_email TEXT,
commit_uri TEXT NOT NULL,
commit_hash TEXT NOT NULL,
commit_branch TEXT NOT NULL,
commit_tag TEXT NOT NULL,
state NUMERIC NOT NULL,
package_name TEXT NOT NULL
);
CREATE TABLE batches
(
name VARCHAR(255) PRIMARY KEY,
batch_id VARCHAR(255) UNIQUE,
create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
seal_time TIMESTAMPTZ,
worker_id TEXT REFERENCES workers (worker_id) NOT NULL,
bugtracker_uri TEXT
);
CREATE TABLE bugtracker_configs
(
name VARCHAR(255) PRIMARY KEY,
create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
update_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
config JSONB NOT NULL
);

View File

@ -1,23 +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_library(
name = "migrations",
srcs = ["migrations.go"],
embedsrcs = ["000001_init.up.sql"],
importpath = "go.resf.org/peridot/tools/mothership/migrations",
visibility = ["//visibility:public"],
)

View File

@ -1,20 +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 mothership_migrations
import "embed"
//go:embed *.up.sql
var UpSQLs embed.FS

View File

@ -1,13 +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.

View File

@ -1,64 +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("@rules_proto//proto:defs.bzl", "proto_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("//tools/build_rules/oapi_gen:defs.bzl", "oapi_gen_ts")
proto_library(
name = "mshipadminpb_proto",
srcs = [
"bugtracker.proto",
"mship_admin.proto",
"worker.proto",
],
visibility = ["//visibility:public"],
deps = [
"@com_google_protobuf//:empty_proto",
"@com_google_protobuf//:timestamp_proto",
"@go_googleapis//google/api:annotations_proto",
"@go_googleapis//google/longrunning:longrunning_proto",
"@googleapis//google/api:annotations_proto",
],
)
oapi_gen_ts(
name = "mshipadminpb_ts_proto",
proto = ":mshipadminpb_proto",
visibility = ["//visibility:public"],
)
go_proto_library(
name = "mshipadminpb_go_proto",
compilers = [
"@io_bazel_rules_go//proto:go_grpc",
"//:go_gen_grpc_gateway",
],
importpath = "go.resf.org/peridot/tools/mothership/admin/pb",
proto = ":mshipadminpb_proto",
visibility = ["//visibility:public"],
deps = [
"//third_party/googleapis/google/longrunning:longrunning_go_proto",
"@go_googleapis//google/api:annotations_go_proto",
"@org_golang_google_genproto//googleapis/api/annotations",
],
)
go_library(
name = "pb",
embed = [":mshipadminpb_go_proto"],
importpath = "go.resf.org/peridot/tools/mothership/admin/pb",
visibility = ["//visibility:public"],
)

View File

@ -1,57 +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.
syntax = "proto3";
package peridot.tools.mothership.admin.v1;
option java_multiple_files = true;
option java_outer_classname = "BugtrackerProto";
option java_package = "org.resf.peridot.tools.mothership.admin.v1";
option go_package = "go.resf.org/peridot/tools/mothership/admin/pb;mshipadminpb";
// BugTrackerConfig is the configuration for a bug tracker.
// Usually, the bug tracker is a third-party service, such as Mantis or Track
// The configuration is used to track import batches
message BugTrackerConfig {
// Supported bug trackers.
enum Type {
// Unknown bug tracker.
UNKNOWN = 0;
// MantisBT bug tracker.
MANTIS = 1;
}
// Type of the bug tracker.
Type type = 1;
// URI of the bug tracker.
string uri = 2;
// Configuration options for MantisBT
message MantisConfig {
// API key for the bug tracker.
string api_key = 1;
// Project ID mapping.
// Maps major version to project ID.
map<int32, int64> project_ids = 2;
}
// Configuration for the bug tracker.
oneof config {
// User-defined configuration for MantisBT.
MantisConfig mantis = 3;
}
}

View File

@ -1,173 +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.
syntax = "proto3";
package peridot.tools.mothership.admin.v1;
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/longrunning/operations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "tools/mothership/proto/admin/v1/worker.proto";
option java_multiple_files = true;
option java_outer_classname = "MshipAdminProto";
option java_package = "org.resf.peridot.tools.mothership.admin.v1";
option go_package = "go.resf.org/peridot/tools/mothership/admin/pb;mshipadminpb";
// Service to manage Mothership/SrpmArchiver instances.
service MshipAdmin {
// Gets a worker
rpc GetWorker(GetWorkerRequest) returns (Worker) {
option (google.api.http) = {
get: "/v1/{name=workers/*}"
};
option (google.api.method_signature) = "name";
}
// Lists the workers registered
rpc ListWorkers(ListWorkersRequest) returns (ListWorkersResponse) {
option (google.api.http) = {
get: "/v1/workers"
};
}
// (-- api-linter: core::0133::http-body=disabled
// aip.dev/not-precedent: See below in the CreateWorkerRequest. We only allow worker_id --)
// Creates a worker
rpc CreateWorker(CreateWorkerRequest) returns (Worker) {
option (google.api.http) = {
post: "/v1/workers"
body: "*"
};
option (google.api.method_signature) = "worker_id";
}
// Deletes a worker
// Worker cannot be deleted if it has created an entry.
rpc DeleteWorker(DeleteWorkerRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/v1/{name=workers/*}"
};
option (google.api.method_signature) = "name";
}
// Rescue an entry import attempt
// This should be called after fixing patches that caused the import to fail.
// This will re-run the import attempt.
rpc RescueEntryImport(RescueEntryImportRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/{name=entries/*}:rescueImport"
};
option (google.api.method_signature) = "name";
}
// Retract the entry
// To be able to retract an entry, the entry must be in the `ARCHIVED` state.
// This will allow an NVR to be re-imported.
rpc RetractEntry(RetractEntryRequest) returns (google.longrunning.Operation) {
option (google.api.http) = {
post: "/v1/{name=entries/*}:retract"
};
option (google.api.method_signature) = "name";
option (google.longrunning.operation_info) = {
response_type: "RetractEntryResponse"
metadata_type: "RetractEntryMetadata"
};
}
}
// GetWorkerRequest is the request message for GetWorker.
message GetWorkerRequest {
// Required. The name of the worker to retrieve.
string name = 1 [(google.api.field_behavior) = REQUIRED];
}
// ListWorkersRequest is the request message for ListWorkers.
message ListWorkersRequest {
// The maximum number of workers to return.
// If not specified, the server will pick an appropriate default.
int32 page_size = 1;
// A page token, received from a previous `ListWorkers` call.
// Provide this to retrieve the subsequent page.
// When paginating, all other parameters provided to `ListWorkers` must match
// the call that provided the page token.
string page_token = 2;
// The filter to apply to list of workers.
// Supports all fields of the `Worker` resource.
string filter = 3;
// The order to apply to the list of workers.
// Supports all fields of the `Worker` resource.
// Needs a suffix of either `asc` or `desc`.
// Example: `name asc`, `created_at desc`.
string order_by = 4;
}
// ListWorkersResponse is the response message for ListWorkers.
message ListWorkersResponse {
// The workers belonging to the requested project.
repeated Worker workers = 1;
// A token, which can be sent as `page_token` to retrieve the next page.
// If this field is omitted, there are no subsequent pages.
string next_page_token = 2;
}
// (-- api-linter: core::0133::request-resource-field=disabled
// aip.dev/not-precedent: There is no reason to require worker as we only allow the worker_id field to be customized. --)
// CreateWorkerRequest is the request message for CreateWorker.
message CreateWorkerRequest {
// Required. The worker name to use.
// This id has to be at least 4 characters long and must be unique.
string worker_id = 1 [(google.api.field_behavior) = REQUIRED];
}
// DeleteWorkerRequest is the request message for DeleteWorker.
message DeleteWorkerRequest {
// Required. The name of the worker to delete.
string name = 1 [(google.api.field_behavior) = REQUIRED];
}
// RescueEntryImportRequest is the request message for RescueEntryImport.
message RescueEntryImportRequest {
// Required. The name of the entry to rescue.
string name = 1 [(google.api.field_behavior) = REQUIRED];
}
// RetractEntryRequest is the request message for RetractEntry.
message RetractEntryRequest {
// Required. The name of the entry to retract.
string name = 1 [(google.api.field_behavior) = REQUIRED];
}
// RetractEntryResponse is the response message for RetractEntry.
message RetractEntryResponse {
// The name of the entry that was retracted.
string name = 1;
}
// RetractEntryMetadata is the metadata message for RetractEntry.
message RetractEntryMetadata {
// The time at which the workflow started
google.protobuf.Timestamp start_time = 1;
// The time at which the workflow finished
google.protobuf.Timestamp end_time = 2;
}

View File

@ -1,51 +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.
syntax = "proto3";
package peridot.tools.mothership.admin.v1;
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
option java_multiple_files = true;
option java_outer_classname = "WorkerProto";
option java_package = "org.resf.peridot.tools.mothership.admin.v1";
option go_package = "go.resf.org/peridot/tools/mothership/admin/pb;mshipadminpb";
// Worker is a client registered with Mothership that can submit SRPMs.
// Usually these clients are servers that run the Linux distro that is being
// archived and staged.
// Only purpose of workers is to submit SRPMs and be able to identify
// who is submitting them.
message Worker {
// Output only. The resource name of the worker.
// Format: `workers/{worker}`
string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
// Unique identifier selected during creation.
// Cannot be changed. Must conform to RFC-1034.
string worker_id = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
// When the worker was created.
google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
// Last check-in time of the worker.
google.protobuf.Timestamp last_checkin_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
// API secret that the worker should use to authenticate itself.
// This is only returned when creating a new worker.
// Can not be retrieved or changed later.
string api_secret = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
}

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.
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("//tools/build_rules/oapi_gen:defs.bzl", "oapi_gen_ts")
proto_library(
name = "mothershippb_proto",
srcs = [
"batch.proto",
"entry.proto",
"process_rpm.proto",
"srpm_archiver.proto",
],
visibility = ["//visibility:public"],
deps = [
"@com_google_protobuf//:empty_proto",
"@com_google_protobuf//:timestamp_proto",
"@com_google_protobuf//:wrappers_proto",
"@go_googleapis//google/api:annotations_proto",
"@go_googleapis//google/longrunning:longrunning_proto",
"@googleapis//google/api:annotations_proto",
],
)
oapi_gen_ts(
name = "mothershippb_ts_proto",
proto = ":mothershippb_proto",
visibility = ["//visibility:public"],
)
go_proto_library(
name = "mothershippb_go_proto",
compilers = [
"@io_bazel_rules_go//proto:go_grpc",
"//:go_gen_grpc_gateway",
],
importpath = "go.resf.org/peridot/tools/mothership/pb",
proto = ":mothershippb_proto",
visibility = ["//visibility:public"],
deps = [
"//third_party/googleapis/google/longrunning:longrunning_go_proto",
"@go_googleapis//google/api:annotations_go_proto",
"@org_golang_google_genproto//googleapis/api/annotations",
],
)
go_library(
name = "pb",
embed = [":mothershippb_go_proto"],
importpath = "go.resf.org/peridot/tools/mothership/pb",
visibility = ["//visibility:public"],
)

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.
syntax = "proto3";
package peridot.tools.mothership.v1;
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option java_multiple_files = true;
option java_outer_classname = "BatchProto";
option java_package = "org.resf.peridot.tools.mothership.v1";
option go_package = "go.resf.org/peridot/tools/mothership/pb;mothershippb";
// Batch is a collection of Entries that were imported closely
// together. It usually indicates an update batch from a single
// source.
message Batch {
// Output only. Unique ID of the batch.
string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
// Custom ID of the batch. Optional
string batch_id = 2;
// Worker ID that created the batch.
string worker_id = 3;
// Output only. Timestamp when the batch was created.
google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
// Output only. Timestamp when the batch was last updated.
google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
// Output only. Timestamp when the batch was sealed.
// Batches are automatically sealed after an hour of inactivity.
google.protobuf.Timestamp seal_time = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
// Output only. Bugtracker URI of the batch.
google.protobuf.StringValue bugtracker_uri = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
}

View File

@ -1,128 +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.
syntax = "proto3";
package peridot.tools.mothership.v1;
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option java_multiple_files = true;
option java_outer_classname = "EntryProto";
option java_package = "org.resf.peridot.tools.mothership.v1";
option go_package = "go.resf.org/peridot/tools/mothership/pb;mothershippb";
// Entry is a single entry in the mothership log.
// This describes the package being archived, which worker archived it,
// when it was archived, if it was in a batch and which OS release value
// the package was pulled from.
message Entry {
// Output only. Unique ID of the entry.
// Format: `entries/{entry_id}`
string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
// Package NEVRA (name-epoch:version-release.arch) of the package being archived.
string entry_id = 2 [(google.api.field_behavior) = IMMUTABLE];
// When the package was archived.
google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = REQUIRED];
// OS release value the package was pulled from.
string os_release = 4 [(google.api.field_behavior) = REQUIRED];
// SHA256 of the package.
// This value is output only as the API server will determine
// the SHA256 of the package.
string sha256_sum = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
// Repository name of the package as in which repository the package was archived from.
string repository = 6 [(google.api.field_behavior) = REQUIRED];
// Worker ID of the worker that archived the package.
// This value is output only as the API server will determine
// the worker ID of the worker that archived the package based
// on the authentication token used to make the request.
// If this field is not set, the package was archived by a
// user instead of a worker.
google.protobuf.StringValue worker_id = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
// Name of the batch the package was archived in.
// If this field is not set, the package was not archived in a batch.
google.protobuf.StringValue batch = 8;
// User email of the user that archived the package.
// This value is output only as the API server will determine
// the user email of the user that archived the package based
// on the authentication token used to make the request.
// If this field is not set, the package was archived by a
// worker instead of a user.
google.protobuf.StringValue user_email = 9 [(google.api.field_behavior) = OUTPUT_ONLY];
// URI to view commit
string commit_uri = 10 [(google.api.field_behavior) = OUTPUT_ONLY];
// Commit hash of the resulting import
string commit_hash = 11 [(google.api.field_behavior) = OUTPUT_ONLY];
// Commit branch of the resulting import
string commit_branch = 12 [(google.api.field_behavior) = OUTPUT_ONLY];
// Commit tag of the resulting import
string commit_tag = 13 [(google.api.field_behavior) = OUTPUT_ONLY];
// Valid states of an entry.
enum State {
// Default value. This value is unused.
STATE_UNSPECIFIED = 0;
// The entry is being archived.
ARCHIVING = 1;
// The entry has been archived.
ARCHIVED = 2;
// One or more errors occurred while archiving the entry.
// Usually related to patches failing to apply.
// The entry will be placed "on hold" until the admin API
// receives the "rescue" call
ON_HOLD = 3;
// Error occurred while archiving the entry and an admin
// cancelled the entry.
// This entry CAN'T be rescued.
CANCELLED = 4;
// Failed to archive the entry.
// This entry CAN'T be rescued.
FAILED = 5;
// Retracting the entry.
RETRACTING = 6;
// Retracted. This entry CAN'T be rescued.
// Another import may have happened, retraction is usually done
// if debranding was not complete but successful.
RETRACTED = 7;
}
// State of the entry.
State state = 14 [(google.api.field_behavior) = OUTPUT_ONLY];
// Name of the package being archived.
string pkg = 15 [(google.api.field_behavior) = OUTPUT_ONLY];
// Error message if on hold
string error_message = 16 [(google.api.field_behavior) = OUTPUT_ONLY];
}

View File

@ -1,105 +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.
syntax = "proto3";
package peridot.tools.mothership.v1;
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "tools/mothership/proto/v1/entry.proto";
option java_multiple_files = true;
option java_outer_classname = "ProcessRpmProto";
option java_package = "org.resf.peridot.tools.mothership.v1";
option go_package = "go.resf.org/peridot/tools/mothership/pb;mothershippb";
// ProcessRPMRequest is the request message for the ProcessRPM workflow
message ProcessRPMRequest {
// URI of the RPM to process
// e.g. gs://bucket/path/to/rpm.rpm
// The server must have read access to the RPM and WILL error if it does not
string rpm_uri = 1 [(google.api.field_behavior) = REQUIRED];
// OS Release of the RPM
// e.g. Red Hat Enterprise Linux release 8.8 (Ootpa)
string os_release = 2 [(google.api.field_behavior) = REQUIRED];
// Self reported checksum of the RPM
// Must be a SHA256 checksum and match the RPM
string checksum = 3 [(google.api.field_behavior) = REQUIRED];
// Self reported repository of the RPM
// e.g. BaseOS
string repository = 4 [(google.api.field_behavior) = REQUIRED];
// Batch to associate the RPM with
string batch = 5;
}
// ProcessRPMInternalRequest is the request message that the Server
// uses in its call to the ProcessRPM workflow
message ProcessRPMInternalRequest {
// Worker ID of the worker processing the RPM
string worker_id = 1 [(google.api.field_behavior) = REQUIRED];
}
// ProcessRPMArgs is the arguments for the ProcessRPM workflow
message ProcessRPMArgs {
// Public request
ProcessRPMRequest request = 1 [(google.api.field_behavior) = REQUIRED];
// Internal request
ProcessRPMInternalRequest internal_request = 2 [(google.api.field_behavior) = REQUIRED];
}
// ProcessRPMMetadata is the metadata for the ProcessRPM workflow
message ProcessRPMMetadata {
// The time at which the workflow started
google.protobuf.Timestamp start_time = 1;
// The time at which the workflow finished
google.protobuf.Timestamp end_time = 2;
}
// ProcessRPMResponse is the response message for the ProcessRPM workflow
message ProcessRPMResponse {
// The entry created for the RPM
Entry entry = 1;
}
// ImportRPMResponse is the response message for the ImportRPM activity
message ImportRPMResponse {
// Commit hash of the imported RPM
// e.g. 1234567890abcdef1234567890abcdef12345678
string commit_hash = 1 [(google.api.field_behavior) = REQUIRED];
// Commit URI of the imported RPM
string commit_uri = 2 [(google.api.field_behavior) = REQUIRED];
// Commit branch of the imported RPM
string commit_branch = 3 [(google.api.field_behavior) = REQUIRED];
// Commit tag of the imported RPM
string commit_tag = 4 [(google.api.field_behavior) = REQUIRED];
// NEVRA of the imported RPM
// e.g. rpm-1.0.0-1.el8.x86_64
string nevra = 5 [(google.api.field_behavior) = REQUIRED];
// Package name of the imported RPM
// e.g. rpm
string pkg = 6 [(google.api.field_behavior) = REQUIRED];
}

View File

@ -1,251 +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.
syntax = "proto3";
package peridot.tools.mothership.v1;
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/longrunning/operations.proto";
import "google/protobuf/empty.proto";
import "tools/mothership/proto/v1/batch.proto";
import "tools/mothership/proto/v1/entry.proto";
import "tools/mothership/proto/v1/process_rpm.proto";
option java_multiple_files = true;
option java_outer_classname = "SrpmArchiverProto";
option java_package = "org.resf.peridot.tools.mothership.v1";
option go_package = "go.resf.org/peridot/tools/mothership/pb;mothershippb";
// SrpmArchiver service is used to archive SRPMs.
// The archived SRPMs is staged in a Git forge. Automatically executing
// other actions post-import is supported.
// The archiver service consists of workers, which are responsible for
// importing SRPMs into the forge.
service SrpmArchiver {
option (google.api.default_host) = "mship.resf.org";
// Returns a batch
rpc GetBatch(GetBatchRequest) returns (Batch) {
option (google.api.http) = {
get: "/v1/{name=batches/*}"
};
option (google.api.method_signature) = "name";
}
// Returns a list of batches that match the filter criteria.
rpc ListBatches(ListBatchesRequest) returns (ListBatchesResponse) {
option (google.api.http) = {
get: "/v1/batches"
};
}
// Creates a batch.
// Only worker credentials can create a batch.
rpc CreateBatch(CreateBatchRequest) returns (Batch) {
option (google.api.http) = {
post: "/v1/batches"
body: "batch"
};
option (google.api.method_signature) = "batch";
}
// Returns an entry
rpc GetEntry(GetEntryRequest) returns (Entry) {
option (google.api.http) = {
get: "/v1/{name=entries/*}"
};
option (google.api.method_signature) = "name";
}
// Returns a list of entries that match the filter criteria.
rpc ListEntries(ListEntriesRequest) returns (ListEntriesResponse) {
option (google.api.http) = {
get: "/v1/entries"
};
}
// Submits an SRPM to be archived.
// A worker can call this method to submit an SRPM to be archived.
// The call can occur even before uploading the SRPM to the object storage
// that way it can be ensured that a certain hash is "leased" by the worker.
// Other workers will still keep the hash in their backlog until the SRPM is
// verified processed.
// Until they can query an entry with `sha256_sum=X` matching the hash of the
// SRPM, it will not be deleted from the backlog.
// If after 2 hours the SRPM is not processed, the worker can assume that
// the SRPM is lost and can be re-uploaded. It that case, the entry will be
// re-assigned to the worker.
// If a checksum can't be leased because it's already being processed,
// AlreadyExists error will be returned.
// The worker MUST stop processing the SRPM in that case.
rpc SubmitEntry(SubmitEntryRequest) returns (google.longrunning.Operation) {
option (google.api.http) = {
post: "/v1/actions:submitEntry"
body: "*"
};
option (google.longrunning.operation_info) = {
response_type: "ProcessRPMResponse"
metadata_type: "ProcessRPMMetadata"
};
}
// WorkerUploadObject is used by workers to upload objects to the
// object storage service.
// Returns AlreadyExists if the SRPM already exists.
// This doesn't necessarily mean that the worker should stop processing,
// especially if it acquired a lease to process this particular SRPM.
rpc WorkerUploadObject(stream WorkerUploadObjectRequest) returns (WorkerUploadObjectResponse) {
option (google.api.http) = {
post: "/v1/actions:workerUploadObject"
body: "chunk"
};
}
// WorkerPing is used by workers to ping the server.
// This is used to check if the worker is still alive.
rpc WorkerPing(google.protobuf.Empty) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/actions:workerPing"
};
}
}
// Request message for GetBatch method.
message GetBatchRequest {
// The name of the batch to retrieve.
// For example: "batches/1234".
string name = 1 [(google.api.field_behavior) = REQUIRED];
}
// Request message for ListBatches method.
message ListBatchesRequest {
// The maximum number of batches to return.
// The service may return fewer than this value.
// If unspecified, at most 50 batches will be returned.
// The maximum value is 1000; values above 1000 will return an error
int32 page_size = 1;
// A page token, received from a previous `ListBatches` call.
// Provide this to retrieve the subsequent page.
//
// When paginating, all other parameters provided to `ListBatches` must
// match the call that provided the page token.
string page_token = 2;
// The filter string, following the syntax described in
// https://google.aip.dev/160.
// Supports all fields of the `Batch` resource.
// Examples:
// - By name: `name="batches/1234"`
string filter = 3;
// The order to sort the results by. For example: `name desc`.
// Supports all fields of the `Batch` resource.
// Needs a suffix of either `asc` or `desc`.
// Example: `name asc`, `created_at desc`.
string order_by = 4;
}
// Response message for ListBatches method.
message ListBatchesResponse {
// The list of batches.
repeated Batch batches = 1;
// A token to retrieve next page of results.
// Pass this value in the
// [ListBatchesRequest.page_token][peridot.tools.mothership.v1.ListBatchesRequest.page_token]
// field in the subsequent call to `ListBatches` method to retrieve the next
// page of results.
string next_page_token = 2;
}
// Request message for CreateBatch method.
message CreateBatchRequest {
// The batch to create.
Batch batch = 1 [(google.api.field_behavior) = REQUIRED];
// Custom ID for the batch. Optional
string batch_id = 2;
}
// Request message for GetEntry method.
message GetEntryRequest {
// The name of the entry to retrieve.
// For example: "entries/1234".
string name = 1 [(google.api.field_behavior) = REQUIRED];
}
// Request message for ListEntries method.
message ListEntriesRequest {
// The maximum number of entries to return.
// The service may return fewer than this value.
// If unspecified, at most 50 entries will be returned.
// The maximum value is 1000; values above 1000 will return an error
int32 page_size = 1;
// A page token, received from a previous `ListEntries` call.
// Provide this to retrieve the subsequent page.
//
// When paginating, all other parameters provided to `ListEntries` must
// match the call that provided the page token.
string page_token = 2;
// The filter string, following the syntax described in
// https://google.aip.dev/160.
// Supports all fields of the `Entry` resource.
// Examples:
// - By name: `name="entries/1234"`
string filter = 3;
// The order to sort the results by. For example: `name desc`.
// Supports all fields of the `Worker` resource.
// Needs a suffix of either `asc` or `desc`.
// Example: `name asc`, `created_at desc`.
string order_by = 4;
}
// Response message for ListEntries method.
message ListEntriesResponse {
// The list of entries.
repeated Entry entries = 1;
// A token to retrieve next page of results.
// Pass this value in the
// [ListEntriesRequest.page_token][peridot.tools.mothership.v1.ListEntriesRequest.page_token]
// field in the subsequent call to `ListEntries` method to retrieve the next
// page of results.
string next_page_token = 2;
}
// Request message for SubmitEntry method.
message SubmitEntryRequest {
// Process request for RPM.
// This request is sent to the worker to process the RPM.
ProcessRPMRequest process_rpm_request = 1 [(google.api.field_behavior) = REQUIRED];
}
// Request message for WorkerUploadObject method.
message WorkerUploadObjectRequest {
// The object to upload.
bytes chunk = 1 [(google.api.field_behavior) = REQUIRED];
}
// Response message for WorkerUploadObject method.
message WorkerUploadObjectResponse {
// The object URI.
string uri = 1 [(google.api.field_behavior) = REQUIRED];
}

View File

@ -1,16 +0,0 @@
<p align="center">
<picture>
<source srcset="ui/mship_gopher.png" media="(prefers-color-scheme: dark)" height="150">
<img src="ui/mship_gopher_dark.png" alt="Mship" height="150">
</picture>
</p>
<p align="center">Tool to archive RPM packages and attest to their authenticity</p>
<hr />
### Development
Using the taskrunner2 target is sufficient. `bazel run //tools/mothership`.
This will watch for changes in mship_admin_server, mship_server and both UIs.
The target will also start Dex IDP and Temporal if not already running.

View File

@ -1,50 +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_library(
name = "rpc",
srcs = [
"batch.go",
"entry.go",
"operation.go",
"ping.go",
"rpc.go",
"worker.go",
],
importpath = "go.resf.org/peridot/tools/mothership/rpc",
visibility = ["//visibility:public"],
deps = [
"//base/go",
"//third_party/googleapis/google/longrunning:longrunning_go_proto",
"//tools/mothership/db",
"//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:status_go_proto",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//metadata",
"@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",
],
)

View File

@ -1,75 +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 mothership_rpc
import (
"context"
"go.ciq.dev/pika"
base "go.resf.org/peridot/base/go"
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"
)
func (s *Server) GetBatch(_ context.Context, req *mothershippb.GetBatchRequest) (*mothershippb.Batch, error) {
batch, err := base.Q[mothership_db.Batch](s.db).F("name", req.Name).GetOrNil()
if err != nil {
base.LogErrorf("failed to get batch: %v", err)
return nil, status.Error(codes.Internal, "failed to get batch")
}
if batch == nil {
return nil, status.Error(codes.NotFound, "batch not found")
}
return batch.ToPB(), nil
}
func (s *Server) ListBatches(_ context.Context, req *mothershippb.ListBatchesRequest) (*mothershippb.ListBatchesResponse, error) {
aipOptions := pika.ProtoReflect(&mothershippb.Batch{})
page, nt, err := base.Q[mothership_db.Batch](s.db).GetPage(req, aipOptions)
if err != nil {
base.LogErrorf("failed to get batch page: %v", err)
return nil, status.Error(codes.Internal, "failed to get batch page")
}
return &mothershippb.ListBatchesResponse{
Batches: base.SliceToPB[*mothershippb.Batch, *mothership_db.Batch](page),
NextPageToken: nt,
}, nil
}
func (s *Server) CreateBatch(ctx context.Context, req *mothershippb.CreateBatchRequest) (*mothershippb.Batch, error) {
worker, err := s.getWorkerIdentity(ctx)
if err != nil {
return nil, err
}
batch := &mothership_db.Batch{
Name: base.NameGen("batches"),
BatchID: req.BatchId,
WorkerID: worker.WorkerID,
}
if err := base.Q[mothership_db.Batch](s.db).Create(batch); err != nil {
base.LogErrorf("failed to create batch: %v", err)
return nil, status.Error(codes.Internal, "failed to create batch")
}
return batch.ToPB(), nil
}

View File

@ -1,136 +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 mothership_rpc
import (
"context"
"go.ciq.dev/pika"
base "go.resf.org/peridot/base/go"
mothership_db "go.resf.org/peridot/tools/mothership/db"
mothershippb "go.resf.org/peridot/tools/mothership/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) GetEntry(ctx context.Context, req *mothershippb.GetEntryRequest) (*mothershippb.Entry, error) {
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")
}
pb := entry.ToPB()
// If on hold, let's query temporal for more info.
if entry.State == mothershippb.Entry_ON_HOLD {
events := s.temporal.GetWorkflowHistory(ctx, "operations/"+entry.Sha256Sum, "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
// We only need to find the latest ImportRPM event.
// Return the error from that event.
pb.ErrorMessage = "Unknown error"
for events.HasNext() {
event, err := events.Next()
if err != nil {
base.LogErrorf("failed to get next event: %v", err)
continue
}
failedAttrs := event.GetActivityTaskFailedEventAttributes()
if failedAttrs == nil {
continue
}
pb.ErrorMessage = failedAttrs.Failure.Message
break
}
}
return pb, nil
}
func (s *Server) ListEntries(_ context.Context, req *mothershippb.ListEntriesRequest) (*mothershippb.ListEntriesResponse, error) {
aipOptions := pika.ProtoReflect(&mothershippb.Entry{})
page, nt, err := base.Q[mothership_db.Entry](s.db).GetPage(req, aipOptions)
if err != nil {
base.LogErrorf("failed to get entry page: %v", err)
return nil, status.Error(codes.Internal, "failed to get entry page")
}
return &mothershippb.ListEntriesResponse{
Entries: base.SliceToPB[*mothershippb.Entry, *mothership_db.Entry](page),
NextPageToken: nt,
}, nil
}
// SubmitEntry handles the RPC request for submitting an entry. This is usually
// called by the worker. The worker must be authenticated. The checksum will "lease"
// the entry for the worker, so that other workers will not submit the same entry.
// This "lease" is enforced using Temporal
func (s *Server) SubmitEntry(ctx context.Context, req *mothershippb.SubmitEntryRequest) (*longrunning.Operation, error) {
worker, err := s.getWorkerIdentity(ctx)
if err != nil {
return nil, err
}
// Now make sure the entry doesn't already exist in the ARCHIVED state.
// If it does, return an error. It should be retracted first.
entry, err := base.Q[mothership_db.Entry](s.db).F(
"sha256_sum", req.ProcessRpmRequest.Checksum,
"state", mothershippb.Entry_ARCHIVED,
).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.AlreadyExists, "entry already exists, you must retract the entry before submitting again")
}
startWorkflowOpts := client.StartWorkflowOptions{
ID: "operations/" + req.ProcessRpmRequest.Checksum,
WorkflowExecutionErrorWhenAlreadyStarted: true,
WorkflowIDReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE,
}
// Submit to Temporal
run, err := s.temporal.ExecuteWorkflow(
context.Background(),
startWorkflowOpts,
mothership_worker_server.ProcessRPMWorkflow,
&mothershippb.ProcessRPMArgs{
Request: req.ProcessRpmRequest,
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: worker.WorkerID,
},
},
)
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,143 +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 mothership_rpc
import (
"context"
base "go.resf.org/peridot/base/go"
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 := &mothershippb.ProcessRPMMetadata{
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,43 +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 mothership_rpc
import (
"context"
"database/sql"
base "go.resf.org/peridot/base/go"
mothership_db "go.resf.org/peridot/tools/mothership/db"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"time"
)
func (s *Server) WorkerPing(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) {
worker, err := s.getWorkerIdentity(ctx)
if err != nil {
return nil, err
}
worker.LastCheckinTime = sql.NullTime{
Time: time.Now(),
Valid: true,
}
if err := base.Q[mothership_db.Worker](s.db).U(worker); err != nil {
return nil, status.Error(codes.Internal, "failed to update worker")
}
return &emptypb.Empty{}, nil
}

View File

@ -1,62 +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 mothership_rpc
import (
base "go.resf.org/peridot/base/go"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"go.temporal.io/sdk/client"
"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc"
)
type Server struct {
base.GRPCServer
mothershippb.UnimplementedSrpmArchiverServer
longrunning.UnimplementedOperationsServer
db *base.DB
temporal client.Client
}
func NewServer(db *base.DB, temporalClient client.Client, opts ...base.GRPCServerOption) (*Server, error) {
opts = append(opts, base.WithServeMuxAdditionalHeaders("x-mship-worker-secret"))
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)
mothershippb.RegisterSrpmArchiverServer(server, s)
})
if err := s.GatewayEndpoints(
longrunning.RegisterOperationsHandler,
mothershippb.RegisterSrpmArchiverHandler,
); err != nil {
return err
}
return s.GRPCServer.Start()
}

View File

@ -1,52 +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 mothership_rpc
import (
"context"
base "go.resf.org/peridot/base/go"
mothership_db "go.resf.org/peridot/tools/mothership/db"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// getWorkerIdentity returns the identity of the worker that the request is
// coming from. Returns an error if the worker is not found or unauthenticated.
func (s *Server) getWorkerIdentity(ctx context.Context) (*mothership_db.Worker, error) {
// Get x-mship-worker-secret
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
secrets := md["x-mship-worker-secret"]
if len(secrets) != 1 {
return nil, status.Error(codes.Unauthenticated, "missing worker secret")
}
secret := secrets[0]
worker, err := base.Q[mothership_db.Worker](s.db).F("api_secret", secret).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.Unauthenticated, "invalid worker secret")
}
return worker, nil
}

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.
*/
import React from 'react';
import { Link, 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 Button from '@mui/material/Button';
import { Theme } from '@mui/material/styles';
import { GetEntry } from 'tools/mothership/ui/GetEntry';
import { Entries } from 'tools/mothership/ui/Entries';
export const App = () => {
return (
<Box sx={{ display: 'flex' }}>
<AppBar
elevation={5}
position="fixed"
sx={{ zIndex: (theme: Theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar variant="dense">
<Link to="/">
<img
alt="Mothership logo"
src={
window.__beta__
? '/_ga/mship_gopher_beta.png'
: '/_ga/mship_gopher.png'
}
height="41.5px"
/>
</Link>
<Box sx={{ flexGrow: 1, textAlign: 'right' }}>
<Button className="native-link" href="/admin" variant="primary">
Admin
</Button>
</Box>
</Toolbar>
</AppBar>
<Box component="main" sx={{ p: 3, flexGrow: 1 }}>
<Toolbar variant="dense" />
<Routes>
<Route index element={<Navigate to="/entries" replace />} />
<Route path="/entries">
<Route index element={<Entries />} />
<Route path=":name" element={<GetEntry />} />
</Route>
</Routes>
</Box>
</Box>
);
};

View File

@ -1,41 +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/v1:mothershippb_ts_proto",
],
)
go_library(
name = "ui",
srcs = ["ui.go"],
# keep
embedsrcs = [
":bundle", # keep
"mship_gopher.png", # keep
"mship_gopher_beta.png", # keep
"favicon.png", # keep
],
importpath = "go.resf.org/peridot/tools/mothership/ui",
visibility = ["//visibility:public"],
deps = ["//base/go"],
)

View File

@ -1,45 +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 { ResourceTable } from 'base/ts/mui/ResourceTable';
import { srpmArchiverApi } from 'tools/mothership/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 (
<ResourceTable<V1Entry>
defaultFilter={'state="ARCHIVED"'}
load={(pageSize: number, pageToken?: string, filter?: string) => reqap(srpmArchiverApi.listEntries({
pageSize,
pageToken,
filter,
}))}
transform={((response: V1ListEntriesResponse) => response.entries || [])}
fields={[
{ key: 'name', label: 'Entry Name' },
{ key: 'entryId', label: 'Entry ID' },
{ key: 'createTime', label: 'Created' },
{ key: 'state', label: 'State' },
]}
/>
);
};

View File

@ -1,84 +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 { 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 { srpmArchiverApi } from 'tools/mothership/ui/api';
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();
}, []);
return (
<Box>
<Box
sx={{
px: 1.5,
height: '48px',
display: 'flex',
justifyContent: 'justify-between',
alignItems: 'center',
}}
>
<span>entries/{params.name}</span>
</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>
</Box>
);
};

View File

@ -1,23 +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 srpmArchiver from 'bazel-bin/tools/mothership/proto/v1/mothershippb_ts_proto_gen';
const cfg = new srpmArchiver.Configuration({
basePath: '/api',
})
export const srpmArchiverApi = new srpmArchiver.SrpmArchiverApi(cfg);

View File

@ -1,34 +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 { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import CssBaseline from '@mui/material/CssBaseline';
import ThemeProvider from '@mui/material/styles/ThemeProvider';
import { App } from './App';
import { peridotDarkTheme } from 'base/ts/mui/theme';
const root = createRoot(document.getElementById('app') || document.body);
root.render(
<BrowserRouter basename={window.__peridot_prefix__ || ''}>
<ThemeProvider theme={peridotDarkTheme}>
<CssBaseline />
<App />
</ThemeProvider>
</BrowserRouter>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,47 +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 mship_ui
import (
"embed"
base "go.resf.org/peridot/base/go"
)
//go:embed *
var assets embed.FS
//go:embed mship_gopher.png
var gopher []byte
//go:embed mship_gopher_beta.png
var gopherBeta []byte
//go:embed favicon.png
var favicon []byte
func InitFrontendInfo(info *base.FrontendInfo) *embed.FS {
if info == nil {
info = &base.FrontendInfo{}
}
info.Title = "Mship"
info.NoAuth = true
info.AdditionalContent = map[string][]byte{
"/_ga/mship_gopher.png": gopher,
"/_ga/mship_gopher_beta.png": gopherBeta,
"/_ga/favicon.png": favicon,
}
return &assets
}

View File

@ -1,13 +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.

View File

@ -1,22 +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_library(
name = "dnf",
srcs = ["dnf.go"],
importpath = "go.resf.org/peridot/tools/mothership/worker_client/dnf",
visibility = ["//visibility:public"],
)

View File

@ -1,19 +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 dnf
type Dnf interface {
GetRepositoryPackages(repo string) ([]string, error)
}

View File

@ -1,99 +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 = "worker_server",
srcs = [
"entry.go",
"process_rpm.go",
"retract_entry.go",
"utils.go",
"worker.go",
"workflows.go",
],
importpath = "go.resf.org/peridot/tools/mothership/worker_server",
visibility = ["//visibility:public"],
deps = [
"//base/go",
"//base/go/forge",
"//base/go/storage",
"//tools/mothership/db",
"//tools/mothership/proto/admin/v1:pb",
"//tools/mothership/proto/v1:pb",
"//tools/mothership/worker_server/srpm_import",
"//vendor/github.com/go-git/go-billy/v5:go-billy",
"//vendor/github.com/go-git/go-billy/v5/memfs",
"//vendor/github.com/go-git/go-git/v5:go-git",
"//vendor/github.com/go-git/go-git/v5/config",
"//vendor/github.com/go-git/go-git/v5/plumbing",
"//vendor/github.com/go-git/go-git/v5/plumbing/object",
"//vendor/github.com/go-git/go-git/v5/plumbing/transport",
"//vendor/github.com/go-git/go-git/v5/storage/memory",
"//vendor/github.com/pkg/errors",
"//vendor/github.com/sassoftware/go-rpmutils",
"//vendor/go.temporal.io/sdk/temporal",
"//vendor/go.temporal.io/sdk/workflow",
"//vendor/golang.org/x/crypto/openpgp",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//status",
],
)
go_test(
name = "worker_server_test",
size = "small",
srcs = [
"entry_test.go",
"forge_test.go",
"main_test.go",
"process_rpm_test.go",
"retract_entry_test.go",
"utils_test.go",
"workflows_test.go",
],
data = glob(["testdata/**"]),
embed = [":worker_server"],
embedsrcs = ["testdata/RPM-GPG-KEY-Rocky-8"],
deps = [
"//base/go",
"//base/go/forge",
"//base/go/storage/memory",
"//tools/mothership/db",
"//tools/mothership/migrations",
"//tools/mothership/proto/admin/v1:pb",
"//tools/mothership/proto/v1:pb",
"//tools/mothership/worker_server/srpm_import",
"//vendor/github.com/go-git/go-billy/v5/memfs",
"//vendor/github.com/go-git/go-billy/v5/osfs",
"//vendor/github.com/go-git/go-git/v5:go-git",
"//vendor/github.com/go-git/go-git/v5/plumbing",
"//vendor/github.com/go-git/go-git/v5/plumbing/cache",
"//vendor/github.com/go-git/go-git/v5/plumbing/object",
"//vendor/github.com/go-git/go-git/v5/plumbing/transport/http",
"//vendor/github.com/go-git/go-git/v5/storage/filesystem",
"//vendor/github.com/go-git/go-git/v5/storage/memory",
"//vendor/github.com/stretchr/testify/mock",
"//vendor/github.com/stretchr/testify/require",
"//vendor/github.com/stretchr/testify/suite",
"//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/log",
"//vendor/go.temporal.io/sdk/temporal",
"//vendor/go.temporal.io/sdk/testsuite",
"//vendor/golang.org/x/crypto/openpgp",
],
)

View File

@ -1,195 +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 mothership_worker_server
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"github.com/pkg/errors"
"github.com/sassoftware/go-rpmutils"
base "go.resf.org/peridot/base/go"
mothership_db "go.resf.org/peridot/tools/mothership/db"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"go.temporal.io/sdk/temporal"
"io"
"os"
"path/filepath"
"time"
)
func (w *Worker) CreateEntry(args *mothershippb.ProcessRPMArgs) (*mothershippb.Entry, error) {
req := args.Request
internalReq := args.InternalRequest
entry := mothership_db.Entry{
Name: base.NameGen("entries"),
OSRelease: req.OsRelease,
Sha256Sum: req.Checksum,
RepositoryName: req.Repository,
WorkerID: sql.NullString{
String: internalReq.WorkerId,
Valid: true,
},
State: mothershippb.Entry_ARCHIVING,
}
if req.Batch != "" {
entry.BatchName = sql.NullString{
String: req.Batch,
Valid: true,
}
}
err := base.Q[mothership_db.Entry](w.db).Create(&entry)
if err != nil {
return nil, errors.Wrap(err, "failed to create entry")
}
return entry.ToPB(), nil
}
// SetEntryIDFromRPM sets the entry ID from the RPM.
// This is a Temporal activity.
func (w *Worker) SetEntryIDFromRPM(entry string, uri string, checksumSha256 string) (*mothershippb.Entry, error) {
ent, err := base.Q[mothership_db.Entry](w.db).F("name", entry).GetOrNil()
if err != nil {
return nil, errors.Wrap(err, "failed to get entry")
}
if ent == nil {
return nil, errors.New("entry does not exist")
}
tempDir, err := os.MkdirTemp("", "mothership-worker-server-import-rpm-*")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(tempDir)
object, err := getObjectPath(uri)
if err != nil {
return nil, err
}
err = w.storage.Download(object, filepath.Join(tempDir, "resource.rpm"))
if err != nil {
return nil, errors.Wrap(err, "failed to download resource")
}
// Verify checksum
hash := sha256.New()
f, err := os.Open(filepath.Join(tempDir, "resource.rpm"))
if err != nil {
return nil, errors.Wrap(err, "failed to open resource")
}
defer f.Close()
if _, err := io.Copy(hash, f); err != nil {
return nil, errors.Wrap(err, "failed to hash resource")
}
if hex.EncodeToString(hash.Sum(nil)) != checksumSha256 {
return nil, temporal.NewNonRetryableApplicationError(
"checksum does not match",
"checksumDoesNotMatch",
errors.New("client submitted a checksum that does not match the resource"),
)
}
// Read the RPM headers
_, err = f.Seek(0, io.SeekStart)
if err != nil {
return nil, errors.Wrap(err, "failed to seek resource")
}
rpm, err := rpmutils.ReadRpm(f)
if err != nil {
return nil, errors.Wrap(err, "failed to read RPM headers")
}
nevra, err := rpm.Header.GetNEVRA()
if err != nil {
return nil, errors.Wrap(err, "failed to get RPM NEVRA")
}
// Set entry ID
ent.EntryID = fmt.Sprintf("%s-%s-%s.src", nevra.Name, nevra.Version, nevra.Release)
ent.Sha256Sum = checksumSha256
// Update entry
if err := base.Q[mothership_db.Entry](w.db).U(ent); err != nil {
return nil, errors.Wrap(err, "failed to update entry")
}
return ent.ToPB(), nil
}
func (w *Worker) SetEntryState(entry string, state mothershippb.Entry_State, importRpmRes *mothershippb.ImportRPMResponse) (*mothershippb.Entry, error) {
ent, err := base.Q[mothership_db.Entry](w.db).F("name", entry).GetOrNil()
if err != nil {
return nil, errors.Wrap(err, "failed to get entry")
}
if ent == nil {
return nil, temporal.NewNonRetryableApplicationError(
"entry does not exist",
"entryDoesNotExist",
errors.New("entry does not exist"),
)
}
ent.State = state
if importRpmRes != nil {
ent.CommitURI = importRpmRes.CommitUri
ent.CommitHash = importRpmRes.CommitHash
ent.CommitBranch = importRpmRes.CommitBranch
ent.CommitTag = importRpmRes.CommitTag
ent.PackageName = importRpmRes.Pkg
}
if err := base.Q[mothership_db.Entry](w.db).U(ent); err != nil {
return nil, errors.Wrap(err, "failed to update entry")
}
return ent.ToPB(), nil
}
func (w *Worker) SetWorkerLastCheckinTime(workerID string) error {
wrk, err := base.Q[mothership_db.Worker](w.db).F("worker_id", workerID).GetOrNil()
if err != nil {
return errors.Wrap(err, "failed to get worker")
}
if wrk == nil {
return temporal.NewNonRetryableApplicationError(
"worker does not exist",
"workerDoesNotExist",
errors.New("worker does not exist"),
)
}
wrk.LastCheckinTime = sql.NullTime{
Time: time.Now(),
Valid: true,
}
return base.Q[mothership_db.Worker](w.db).U(wrk)
}
func (w *Worker) DeleteEntry(name string) error {
err := base.Q[mothership_db.Entry](w.db).F("name", name).Delete()
if err != nil {
if err == sql.ErrNoRows {
return nil
}
return errors.Wrap(err, "failed to delete entry")
}
return nil
}

View File

@ -1,232 +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 mothership_worker_server
import (
"github.com/stretchr/testify/require"
mothership_db "go.resf.org/peridot/tools/mothership/db"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"testing"
"time"
)
func TestWorker_CreateEntry(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
args := &mothershippb.ProcessRPMArgs{
Request: &mothershippb.ProcessRPMRequest{
RpmUri: "memory://efi-rpm-macros-3-3.el8.src.rpm",
OsRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Checksum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
Repository: "BaseOS",
},
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: "test-worker",
},
}
entry, err := testW.CreateEntry(args)
require.Nil(t, err)
require.NotNil(t, entry)
require.Equal(t, "Rocky Linux release 8.8 (Green Obsidian)", entry.OsRelease)
require.Equal(t, "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28", entry.Sha256Sum)
c, err := q[mothership_db.Entry]().F("name", entry.Name).Count()
require.Nil(t, err)
require.Equal(t, c, 1)
}
func TestWorker_SetEntryIDFromRPM(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
args := &mothershippb.ProcessRPMArgs{
Request: &mothershippb.ProcessRPMRequest{
RpmUri: "memory://efi-rpm-macros-3-3.el8.src.rpm",
OsRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Checksum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
Repository: "BaseOS",
},
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: "test-worker",
},
}
entry, err := testW.CreateEntry(args)
require.Nil(t, err)
require.NotNil(t, entry)
entry, err = testW.SetEntryIDFromRPM(entry.Name, "memory://efi-rpm-macros-3-3.el8.src.rpm", entry.Sha256Sum)
require.Nil(t, err)
require.NotNil(t, entry)
require.Equal(t, "efi-rpm-macros-3-3.el8.src", entry.EntryId)
}
func TestWorker_SetEntryIDFromRPM_FailedToDownload(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
args := &mothershippb.ProcessRPMArgs{
Request: &mothershippb.ProcessRPMRequest{
RpmUri: "memory://not-found.rpm",
OsRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Checksum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
Repository: "BaseOS",
},
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: "test-worker",
},
}
entry, err := testW.CreateEntry(args)
require.Nil(t, err)
require.NotNil(t, entry)
entry, err = testW.SetEntryIDFromRPM(entry.Name, "memory://not-found.rpm", entry.Sha256Sum)
require.NotNil(t, err)
require.Contains(t, err.Error(), "failed to download resource")
}
func TestWorker_SetEntryState(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
args := &mothershippb.ProcessRPMArgs{
Request: &mothershippb.ProcessRPMRequest{
RpmUri: "memory://efi-rpm-macros-3-3.el8.src.rpm",
OsRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Checksum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
Repository: "BaseOS",
},
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: "test-worker",
},
}
entry, err := testW.CreateEntry(args)
require.Nil(t, err)
require.NotNil(t, entry)
importRpmRes := &mothershippb.ImportRPMResponse{
CommitHash: "123",
CommitUri: "https://testforge.resf.org/peridot/efi-rpm-macros/commit/123",
CommitBranch: "el-8.8",
CommitTag: "imports/el-8.8/efi-rpm-macros-3-3.el8",
Nevra: "efi-rpm-macros-0:3-3.el8.aarch64",
Pkg: "efi-rpm-macros",
}
entry, err = testW.SetEntryState(entry.Name, mothershippb.Entry_ARCHIVED, importRpmRes)
require.Nil(t, err)
require.NotNil(t, entry)
require.Equal(t, mothershippb.Entry_ARCHIVED, entry.State)
require.Equal(t, "123", entry.CommitHash)
require.Equal(t, "https://testforge.resf.org/peridot/efi-rpm-macros/commit/123", entry.CommitUri)
require.Equal(t, "el-8.8", entry.CommitBranch)
require.Equal(t, "imports/el-8.8/efi-rpm-macros-3-3.el8", entry.CommitTag)
require.Equal(t, "efi-rpm-macros", entry.Pkg)
}
func TestWorker_SetEntryState_NoRes(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
args := &mothershippb.ProcessRPMArgs{
Request: &mothershippb.ProcessRPMRequest{
RpmUri: "memory://efi-rpm-macros-3-3.el8.src.rpm",
OsRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Checksum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
Repository: "BaseOS",
},
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: "test-worker",
},
}
entry, err := testW.CreateEntry(args)
require.Nil(t, err)
require.NotNil(t, entry)
entry, err = testW.SetEntryState(entry.Name, mothershippb.Entry_ON_HOLD, nil)
require.Nil(t, err)
require.NotNil(t, entry)
require.Equal(t, mothershippb.Entry_ON_HOLD, entry.State)
require.Equal(t, "", entry.CommitHash)
require.Equal(t, "", entry.CommitUri)
require.Equal(t, "", entry.CommitBranch)
require.Equal(t, "", entry.CommitTag)
require.Equal(t, "", entry.Pkg)
}
func TestWorker_SetEntryState_NoEntry(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
entry, err := testW.SetEntryState("entries/123", mothershippb.Entry_ON_HOLD, nil)
require.Nil(t, entry)
require.NotNil(t, err)
require.Contains(t, err.Error(), "entry does not exist")
}
func TestWorker_SetWorkerLastCheckinTime(t *testing.T) {
require.Nil(t, testW.SetWorkerLastCheckinTime("test-worker"))
// Verify that the worker last checkin time is at most 15 seconds ago.
w, err := q[mothership_db.Worker]().F("worker_id", "test-worker").GetOrNil()
require.Nil(t, err)
require.NotNil(t, w)
require.True(t, w.LastCheckinTime.Valid)
require.WithinDuration(t, w.LastCheckinTime.Time, time.Now(), 15*time.Second)
}
func TestWorker_SetWorkerLastCheckinTime_NotFound(t *testing.T) {
err := testW.SetWorkerLastCheckinTime("not-found")
require.NotNil(t, err)
require.Contains(t, err.Error(), "worker does not exist")
}
func TestWorker_DeleteEntry(t *testing.T) {
require.Nil(t, q[mothership_db.Entry]().Delete())
defer func() {
require.Nil(t, q[mothership_db.Entry]().Delete())
}()
args := &mothershippb.ProcessRPMArgs{
Request: &mothershippb.ProcessRPMRequest{
RpmUri: "memory://efi-rpm-macros-3-3.el8.src.rpm",
OsRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Checksum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
Repository: "BaseOS",
},
InternalRequest: &mothershippb.ProcessRPMInternalRequest{
WorkerId: "test-worker",
},
}
entry, err := testW.CreateEntry(args)
require.Nil(t, err)
require.NotNil(t, entry)
err = testW.DeleteEntry(entry.Name)
require.Nil(t, err)
c, err := q[mothership_db.Entry]().F("name", entry.Name).Count()
require.Nil(t, err)
require.Equal(t, c, 0)
}

View File

@ -1,103 +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 mothership_worker_server
import (
"errors"
"fmt"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/cache"
transport_http "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/filesystem"
"go.resf.org/peridot/base/go/forge"
"path/filepath"
"time"
)
type inMemoryForge struct {
localTempDir string
repos map[string]bool
remoteBaseURL string
invalidUsernamePass bool
noAuthMethod bool
}
func (f *inMemoryForge) GetAuthenticator() (*forge.Authenticator, error) {
ret := &forge.Authenticator{
AuthMethod: &transport_http.BasicAuth{
Username: "user",
Password: "pass",
},
AuthorName: "Test User",
AuthorEmail: "test@resf.org",
Expires: time.Now().Add(time.Hour),
}
if f.noAuthMethod {
ret.AuthMethod = nil
} else if f.invalidUsernamePass {
ret.AuthMethod = &transport_http.BasicAuth{
Username: "invalid",
Password: "invalid",
}
}
return ret, nil
}
func (f *inMemoryForge) GetRemote(repo string) string {
return fmt.Sprintf("file://%s/%s", f.localTempDir, repo)
}
func (f *inMemoryForge) GetCommitViewerURL(repo string, commit string) string {
return f.remoteBaseURL + "/" + repo + "/commit/" + commit
}
func (f *inMemoryForge) EnsureRepositoryExists(auth *forge.Authenticator, repo string) error {
// Try casting auth.AuthMethod to *transport_http.BasicAuth
// If it fails, return an error
authx, ok := auth.AuthMethod.(*transport_http.BasicAuth)
if !ok {
return errors.New("auth failed")
}
if authx.Username != "user" || authx.Password != "pass" {
return errors.New("username or password incorrect")
}
if f.repos[repo] {
return nil
}
osfsTemp := osfs.New(filepath.Join(f.localTempDir, repo))
dot, err := osfsTemp.Chroot(".git")
if err != nil {
return err
}
filesystemTemp := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
err = filesystemTemp.Init()
if err != nil {
return err
}
_, err = git.Init(filesystemTemp, nil)
if err != nil {
return err
}
f.repos[repo] = true
return nil
}

View File

@ -1,164 +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 mothership_worker_server
import (
"bytes"
"context"
_ "embed"
"github.com/go-git/go-billy/v5/osfs"
"github.com/stretchr/testify/suite"
"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"
storage_memory "go.resf.org/peridot/base/go/storage/memory"
mothership_db "go.resf.org/peridot/tools/mothership/db"
mothership_migrations "go.resf.org/peridot/tools/mothership/migrations"
"go.temporal.io/sdk/log"
"go.temporal.io/sdk/testsuite"
"golang.org/x/crypto/openpgp"
"os"
"path/filepath"
"testing"
"time"
)
var (
testW *Worker
testWRolling *Worker
//go:embed testdata/RPM-GPG-KEY-Rocky-8
rocky8GpgKey []byte
inmf *inMemoryForge
tempDirForge string
)
type UnitTestSuite struct {
suite.Suite
testsuite.WorkflowTestSuite
env *testsuite.TestWorkflowEnvironment
}
type noopLogger struct{}
func (n *noopLogger) Debug(string, ...any) {}
func (n *noopLogger) Info(string, ...any) {}
func (n *noopLogger) Warn(string, ...any) {}
func (n *noopLogger) Error(string, ...any) {}
func (n *noopLogger) With(...any) log.Logger {
return n
}
func (s *UnitTestSuite) SetupTest() {
s.env = s.NewTestWorkflowEnvironment()
}
func (s *UnitTestSuite) AfterTest(suiteName, testName string) {
s.env.AssertExpectations(s.T())
}
func TestUnitTestSuite(t *testing.T) {
ts := new(UnitTestSuite)
ts.SetLogger(&noopLogger{})
suite.Run(t, ts)
}
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)
}
// Get current working directory
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
lookasideFS := osfs.New("/")
inMemStorage := storage_memory.New(lookasideFS, filepath.Join(cwd, "testdata"))
var gpgKeys openpgp.EntityList
keyRing, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(rocky8GpgKey))
if err != nil {
panic(err)
}
gpgKeys = append(gpgKeys, keyRing...)
tempDirForge, err = os.MkdirTemp("", "test-forge-*")
if err != nil {
panic(err)
}
defer os.RemoveAll(tempDirForge)
inmf = &inMemoryForge{
remoteBaseURL: "https://testforge.resf.org",
localTempDir: tempDirForge,
repos: map[string]bool{},
}
testW = New(db, inMemStorage, gpgKeys, inmf, false)
testWRolling = New(db, inMemStorage, gpgKeys, inmf, true)
if err := q[mothership_db.Worker]().Create(&mothership_db.Worker{
Name: base.NameGen("workers"),
WorkerID: "test-worker",
ApiSecret: "test-secret",
}); err != nil {
panic(err)
}
os.Exit(m.Run())
}
func q[T any]() base.Pika[T] {
return base.Q[T](testW.db)
}

View File

@ -1,175 +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 mothership_worker_server
import (
"crypto/sha256"
"encoding/hex"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
"github.com/sassoftware/go-rpmutils"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"go.resf.org/peridot/tools/mothership/worker_server/srpm_import"
"go.temporal.io/sdk/temporal"
"io"
"os"
"path/filepath"
"strings"
)
// VerifyResourceExists verifies that the resource exists.
// This is a Temporal activity.
func (w *Worker) VerifyResourceExists(uri string) error {
canRead, err := w.storage.CanReadURI(uri)
if err != nil {
return errors.Wrap(err, "failed to check if resource URI can be read")
}
if !canRead {
return temporal.NewNonRetryableApplicationError(
"cannot read resource URI",
"cannotReadResourceURI",
errors.New("client submitted a resource URI that cannot be read by server"),
)
}
object, err := getObjectPath(uri)
if err != nil {
return err
}
exists, err := w.storage.Exists(object)
if err != nil {
return errors.Wrap(err, "failed to check if resource exists")
}
if !exists {
// Since the client can trigger this activity before uploading the resource,
// we should not return a non-retryable error.
// The parent workflow should handle the retry arrangements up to 2 hours
// per the spec.
return errors.New("resource does not exist")
}
return nil
}
// ImportRPM imports an RPM into the database.
// This is a Temporal activity.
func (w *Worker) ImportRPM(uri string, checksumSha256 string, osRelease string) (*mothershippb.ImportRPMResponse, error) {
tempDir, err := os.MkdirTemp("", "mothership-worker-server-import-rpm-*")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(tempDir)
// Parse uri
object, err := getObjectPath(uri)
if err != nil {
return nil, err
}
// Download the resource to the temporary directory
err = w.storage.Download(object, filepath.Join(tempDir, "resource.rpm"))
if err != nil {
return nil, errors.Wrap(err, "failed to download resource")
}
// Verify checksum
hash := sha256.New()
f, err := os.Open(filepath.Join(tempDir, "resource.rpm"))
if err != nil {
return nil, errors.Wrap(err, "failed to open resource")
}
defer f.Close()
if _, err := io.Copy(hash, f); err != nil {
return nil, errors.Wrap(err, "failed to hash resource")
}
if hex.EncodeToString(hash.Sum(nil)) != checksumSha256 {
return nil, temporal.NewNonRetryableApplicationError(
"checksum does not match",
"checksumDoesNotMatch",
errors.New("client submitted a checksum that does not match the resource"),
)
}
// Read the RPM headers
_, err = f.Seek(0, io.SeekStart)
if err != nil {
return nil, errors.Wrap(err, "failed to seek resource")
}
rpm, err := rpmutils.ReadRpm(f)
if err != nil {
return nil, errors.Wrap(err, "failed to read RPM headers")
}
nevra, err := rpm.Header.GetNEVRA()
if err != nil {
return nil, errors.Wrap(err, "failed to get RPM NEVRA")
}
// Ensure repository exists
repoName := nevra.Name
// First ensure that the repo exists.
authenticator, err := w.forge.GetAuthenticator()
if err != nil {
return nil, errors.Wrap(err, "failed to get forge authenticator")
}
err = w.forge.EnsureRepositoryExists(authenticator, repoName)
if err != nil {
return nil, errors.Wrap(err, "failed to ensure repository exists")
}
// Then do an import
srpmState, err := srpm_import.FromFile(filepath.Join(tempDir, "resource.rpm"), w.rolling, w.gpgKeys...)
if err != nil {
if strings.Contains(err.Error(), "failed to verify RPM") {
return nil, temporal.NewNonRetryableApplicationError(
"failed to verify RPM",
"failedToVerifyRPM",
err,
)
}
return nil, errors.Wrap(err, "failed to import SRPM")
}
defer srpmState.Close()
srpmState.SetAuthor(authenticator.AuthorName, authenticator.AuthorEmail)
cloneOpts := &git.CloneOptions{
URL: w.forge.GetRemote(repoName),
Auth: authenticator.AuthMethod,
}
storer := memory.NewStorage()
fs := memfs.New()
importOut, err := srpmState.Import(cloneOpts, storer, fs, w.storage, osRelease)
if err != nil {
return nil, errors.Wrap(err, "failed to import SRPM")
}
commitURI := w.forge.GetCommitViewerURL(repoName, importOut.Commit.Hash.String())
return &mothershippb.ImportRPMResponse{
CommitHash: importOut.Commit.Hash.String(),
CommitUri: commitURI,
CommitBranch: importOut.Branch,
CommitTag: importOut.Tag,
Nevra: nevra.String(),
Pkg: nevra.Name,
}, nil
}

View File

@ -1,133 +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 mothership_worker_server
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestWorker_VerifyResourceExists(t *testing.T) {
require.Nil(t, testW.VerifyResourceExists("memory://efi-rpm-macros-3-3.el8.src.rpm"))
}
func TestWorker_VerifyResourceExists_NotFound(t *testing.T) {
err := testW.VerifyResourceExists("memory://not-found.rpm")
require.NotNil(t, err)
require.Equal(t, err.Error(), "resource does not exist")
}
func TestWorker_VerifyResourceExists_CannotRead(t *testing.T) {
err := testW.VerifyResourceExists("bad-protocol://not-found.rpm")
require.NotNil(t, err)
require.Contains(t, err.Error(), "client submitted a resource URI that cannot be read by server")
}
func TestWorker_ImportRPM(t *testing.T) {
require.False(t, inmf.repos["efi-rpm-macros"])
res, err := testW.ImportRPM(
"memory://efi-rpm-macros-3-3.el8.src.rpm",
"518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
"Rocky Linux release 8.8 (Green Obsidian)",
)
require.Nil(t, err)
require.NotNil(t, res)
require.Equal(t, "efi-rpm-macros", res.Pkg)
require.Equal(t, "efi-rpm-macros-0:3-3.el8.noarch.rpm", res.Nevra)
require.True(t, inmf.repos["efi-rpm-macros"])
}
func TestWorker_ImportRPM_Existing(t *testing.T) {
require.False(t, inmf.repos["basesystem"])
res, err := testW.ImportRPM(
"memory://basesystem-11-5.el8.src.rpm",
"6beff4cbfd5425e2c193312a9a184969a27d6bbd2d4cc29d7ce72dbe3d9f6416",
"Rocky Linux release 8.8 (Green Obsidian)",
)
require.Nil(t, err)
require.NotNil(t, res)
require.True(t, inmf.repos["basesystem"])
res, err = testW.ImportRPM(
"memory://basesystem-11-5.el8.src.rpm",
"6beff4cbfd5425e2c193312a9a184969a27d6bbd2d4cc29d7ce72dbe3d9f6416",
"Rocky Linux release 8.8 (Green Obsidian)",
)
require.Nil(t, err)
require.NotNil(t, res)
require.True(t, inmf.repos["basesystem"])
remote := testW.forge.GetRemote("basesystem")
repo, err := getRepo(remote, nil)
require.Nil(t, err)
commitIter, err := repo.CommitObjects()
require.Nil(t, err)
c, err := commitIter.Next()
require.Nil(t, err)
require.NotNil(t, c)
require.Equal(t, "import basesystem-11-5.el8", c.Message)
c, err = commitIter.Next()
require.Nil(t, err)
require.NotNil(t, c)
require.Equal(t, "import basesystem-11-5.el8", c.Message)
}
func TestWorker_ImportRPM_ChecksumDoesntMatch(t *testing.T) {
res, err := testW.ImportRPM(
"memory://efi-rpm-macros-3-3.el8.src.rpm",
"518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d27",
"Rocky Linux release 8.8 (Green Obsidian)",
)
require.NotNil(t, err)
require.Nil(t, res)
require.Contains(t, err.Error(), "checksum does not match")
}
func TestWorker_ImportRPM_AuthError(t *testing.T) {
inmf.noAuthMethod = true
res, err := testW.ImportRPM(
"memory://efi-rpm-macros-3-3.el8.src.rpm",
"518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
"Rocky Linux release 8.8 (Green Obsidian)",
)
require.NotNil(t, err)
require.Nil(t, res)
require.Contains(t, err.Error(), "auth failed")
inmf.noAuthMethod = false
}
func TestWorker_ImportRPM_InvalidCredentials(t *testing.T) {
inmf.invalidUsernamePass = true
res, err := testW.ImportRPM(
"memory://efi-rpm-macros-3-3.el8.src.rpm",
"518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
"Rocky Linux release 8.8 (Green Obsidian)",
)
require.NotNil(t, err)
require.Nil(t, res)
require.Contains(t, err.Error(), "username or password incorrect")
inmf.invalidUsernamePass = false
}

View File

@ -1,349 +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 mothership_worker_server
import (
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
base "go.resf.org/peridot/base/go"
"go.resf.org/peridot/base/go/forge"
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothership_db "go.resf.org/peridot/tools/mothership/db"
"go.temporal.io/sdk/temporal"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"io"
"os"
"strings"
"time"
)
// getRepo gets a git repository from a remote
// It clones into an in-memory filesystem
func getRepo(remote string, auth transport.AuthMethod) (*git.Repository, error) {
// Just use in memory storage for all repos
storer := memory.NewStorage()
fs := memfs.New()
repo, err := git.Init(storer, fs)
if err != nil {
return nil, err
}
// Add a new remote
refspec := config.RefSpec("refs/*:refs/*")
_, err = repo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{remote},
Fetch: []config.RefSpec{refspec},
})
if err != nil {
return nil, err
}
// Fetch all the refs from the remote
err = repo.Fetch(&git.FetchOptions{
RemoteName: "origin",
Force: true,
RefSpecs: []config.RefSpec{refspec},
Tags: git.AllTags,
Auth: auth,
})
if err != nil {
return nil, err
}
return repo, nil
}
// clonePathToFS clones a path from one filesystem to another
func clonePathToFS(fromFS billy.Filesystem, toFS billy.Filesystem, rootPath string) error {
// check if root directory exists
_, err := fromFS.Stat(rootPath)
if err != nil {
// we don't care if the directory doesn't exist
if os.IsNotExist(err) {
return nil
}
return err
}
// read the root directory
rootDir, err := fromFS.ReadDir(rootPath)
if err != nil {
return err
}
// iterate over the files
for _, file := range rootDir {
// get the file path
filePath := rootPath + "/" + file.Name()
// check if the file is a directory
if file.IsDir() {
// create the directory in the toFS
err = toFS.MkdirAll(filePath, 0755)
if err != nil {
return err
}
// recursively call this function
err = clonePathToFS(fromFS, toFS, filePath)
if err != nil {
return err
}
} else {
// open the file
f, err := fromFS.OpenFile(filePath, os.O_RDONLY, 0644)
if err != nil {
return err
}
defer f.Close()
// create the file in the toFS
toFile, err := toFS.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return err
}
defer toFile.Close()
// copy the file contents
_, err = io.Copy(toFile, f)
if err != nil {
return err
}
}
}
return nil
}
// clonePatchesToTemporaryFS clones the PATCHES directory to a temporary filesystem
// PATCHES directory is the only directory that should survive a retraction
func clonePatchesToTemporaryFS(currentFS billy.Filesystem) (billy.Filesystem, error) {
// create a new in-memory filesystem
fs := memfs.New()
// clone the current filesystem to the new filesystem
err := clonePathToFS(currentFS, fs, "PATCHES")
if err != nil {
return nil, err
}
return fs, nil
}
func resetRepoToPoint(repo *git.Repository, authenticator *forge.Authenticator, commit string) error {
wt, err := repo.Worktree()
if err != nil {
return errors.Wrap(err, "failed to get worktree")
}
// Let's find out the commit before the commit we want to revert
log, err := repo.Log(&git.LogOptions{
From: plumbing.NewHash(commit),
Order: git.LogOrderCommitterTime,
})
if err != nil {
return errors.Wrap(err, "failed to get log")
}
// log.Next() x2 should be the commit we want to revert
targetCommit, err := log.Next()
if err != nil {
return errors.Wrap(err, "failed to get next commit x1")
}
resetToCommit, err := log.Next()
if err != nil {
return errors.Wrap(err, "failed to get next commit x2")
}
// Also get all commits that touches the PATCHES directory
// until the commit we want to revert
firstLog, err := repo.Log(&git.LogOptions{
Order: git.LogOrderCommitterTime,
PathFilter: func(s string) bool {
// Only include PATCHES
if strings.HasPrefix(s, "PATCHES") {
return true
}
return false
},
// Limit to until the commit we want to revert
Since: &resetToCommit.Author.When,
})
if err != nil {
return errors.Wrap(err, "failed to get log")
}
// Get all authors of the commits, since we're going to copy PATCHES
// back into the repo
var commits []*object.Commit
for {
c, err := firstLog.Next()
if err != nil {
if err == io.EOF {
break
}
return errors.Wrap(err, "failed to get next commit")
}
// If the commit was created before the resetToCommit, then we don't want to include it
if c.Author.When.Before(targetCommit.Author.When) {
// Breaking because the commits are sorted by date, so if we encounter a commit that was created before the resetToCommit,
// then all the following commits will be created before the resetToCommit
break
}
commits = append(commits, c)
}
// Copy PATCHES into a temporary filesystem
patchesFS, err := clonePatchesToTemporaryFS(wt.Filesystem)
if err != nil {
return errors.Wrap(err, "failed to clone PATCHES")
}
// reset the repo
err = wt.Reset(&git.ResetOptions{
Commit: resetToCommit.Hash,
Mode: git.HardReset,
})
if err != nil {
return errors.Wrap(err, "failed to reset repo")
}
// Copy PATCHES back into the repo
err = clonePathToFS(patchesFS, wt.Filesystem, "PATCHES")
if err != nil {
return errors.Wrap(err, "failed to copy PATCHES")
}
// If there are diffs, then create a commit consisting of the joined messages and authors
// of the commits that touches the PATCHES directory
if len(commits) > 0 {
// Add the files
_, err = wt.Add(".")
if err != nil {
return errors.Wrap(err, "failed to add PATCHES")
}
// Get the commit message
commitMsg := "Retract \"" + targetCommit.Message + "\"\n\nFast-forwarded following commits:\n"
for _, c := range commits {
commitMsg += c.Message + "\n"
}
commitMsg += "\n"
// Add the authors as "Co-authored-by"
authors := make(map[string]bool)
for _, c := range commits {
authors[c.Author.Name+" <"+c.Author.Email+">"] = true
}
for author := range authors {
commitMsg += "Co-authored-by: " + author + "\n"
}
// Create the commit
_, err = wt.Commit(commitMsg, &git.CommitOptions{
Author: &object.Signature{
Name: authenticator.AuthorName,
Email: authenticator.AuthorEmail,
When: time.Now(),
},
})
if err != nil {
return errors.Wrap(err, "failed to commit")
}
}
return err
}
func (w *Worker) RetractEntry(name string) (*mshipadminpb.RetractEntryResponse, error) {
entry, err := base.Q[mothership_db.Entry](w.db).F("name", 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, temporal.NewNonRetryableApplicationError(
"entry not found",
"entryNotFound",
nil,
)
}
// Get the repo
remote := w.forge.GetRemote(entry.PackageName)
auth, err := w.forge.GetAuthenticator()
if err != nil {
base.LogErrorf("failed to get forge authenticator: %v", err)
return nil, status.Error(codes.Internal, "failed to get forge authenticator")
}
repo, err := getRepo(remote, auth.AuthMethod)
if err != nil {
base.LogErrorf("failed to get repo: %v", err)
return nil, status.Error(codes.Internal, "failed to get repo")
}
// Checkout the entry branch
wt, err := repo.Worktree()
if err != nil {
base.LogErrorf("failed to get worktree: %v", err)
return nil, status.Error(codes.Internal, "failed to get worktree")
}
err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(entry.CommitBranch),
Force: true,
})
if err != nil {
base.LogErrorf("failed to checkout branch: %v", err)
return nil, status.Error(codes.Internal, "failed to checkout branch")
}
// Reset the repo to the commit before the commit we want to revert
err = resetRepoToPoint(repo, auth, entry.CommitHash)
if err != nil {
base.LogErrorf("failed to reset repo: %v", err)
return nil, status.Error(codes.Internal, "failed to reset repo")
}
// Push the changes
err = repo.Push(&git.PushOptions{
RemoteName: "origin",
Force: true,
Auth: auth.AuthMethod,
RefSpecs: []config.RefSpec{
config.RefSpec("refs/heads/" + entry.CommitBranch + ":refs/heads/" + entry.CommitBranch),
},
})
if err != nil {
base.LogErrorf("failed to push changes: %v", err)
return nil, status.Error(codes.Internal, "failed to push changes")
}
return &mshipadminpb.RetractEntryResponse{
Name: entry.Name,
}, nil
}

View File

@ -1,370 +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 mothership_worker_server
import (
"database/sql"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/stretchr/testify/require"
base "go.resf.org/peridot/base/go"
"go.resf.org/peridot/base/go/forge"
storage_memory "go.resf.org/peridot/base/go/storage/memory"
mothership_db "go.resf.org/peridot/tools/mothership/db"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"go.resf.org/peridot/tools/mothership/worker_server/srpm_import"
"io"
"os"
"path/filepath"
"testing"
"time"
)
func TestGetRepo(t *testing.T) {
s, err := srpm_import.FromFile("testdata/efi-rpm-macros-3-3.el8.src.rpm", false)
require.Nil(t, err)
tempDir, err := os.MkdirTemp("", "peridot-srpm-import-test-*")
require.Nil(t, err)
// Create a bare repo in tempDir
osfsTemp := osfs.New(tempDir)
dot, err := osfsTemp.Chroot(".git")
require.Nil(t, err)
filesystemTemp := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
require.Nil(t, filesystemTemp.Init())
_, err = git.Init(filesystemTemp, nil)
require.Nil(t, err)
opts := &git.CloneOptions{
URL: tempDir,
}
storer := memory.NewStorage()
fs := memfs.New()
lookaside := storage_memory.New(osfs.New("/"))
_, err = s.Import(opts, storer, fs, lookaside, "")
require.Nil(t, err)
// Check that the repo was created
repo, err := getRepo("file://"+tempDir, nil)
require.Nil(t, err)
require.NotNil(t, repo)
// Check that the repo was cloned
commits, err := repo.CommitObjects()
require.Nil(t, err)
require.NotNil(t, commits)
commit, err := commits.Next()
require.Nil(t, err)
require.NotNil(t, commit)
require.Equal(t, "import efi-rpm-macros-3-3.el8", commit.Message)
}
func TestClonePathToFS(t *testing.T) {
fromFS := memfs.New()
toFS := memfs.New()
f, err := fromFS.OpenFile("test", os.O_CREATE|os.O_RDWR, 0644)
require.Nil(t, err)
_, err = f.Write([]byte("test"))
require.Nil(t, err)
err = f.Close()
require.Nil(t, err)
err = clonePathToFS(fromFS, toFS, ".")
require.Nil(t, err)
f, err = toFS.OpenFile("test", os.O_RDONLY, 0644)
require.Nil(t, err)
b, err := io.ReadAll(f)
require.Nil(t, err)
require.Equal(t, "test", string(b))
}
func TestClonePathToFS_RootPath(t *testing.T) {
fromFS := memfs.New()
toFS := memfs.New()
f, err := fromFS.OpenFile("test", os.O_CREATE|os.O_RDWR, 0644)
require.Nil(t, err)
_, err = f.Write([]byte("test"))
require.Nil(t, err)
err = f.Close()
require.Nil(t, err)
f, err = fromFS.OpenFile("testdir/foo", os.O_CREATE|os.O_RDWR, 0644)
require.Nil(t, err)
_, err = f.Write([]byte("bar"))
require.Nil(t, err)
err = f.Close()
require.Nil(t, err)
err = clonePathToFS(fromFS, toFS, "testdir")
require.Nil(t, err)
f, err = toFS.OpenFile("testdir/foo", os.O_RDONLY, 0644)
require.Nil(t, err)
b, err := io.ReadAll(f)
require.Nil(t, err)
require.Equal(t, "bar", string(b))
f, err = toFS.OpenFile("test", os.O_RDONLY, 0644)
require.NotNil(t, err)
require.Equal(t, "file does not exist", err.Error())
}
func TestPatchesToTemporaryFS(t *testing.T) {
fromFS := memfs.New()
f, err := fromFS.OpenFile("PATCHES/test", os.O_CREATE|os.O_RDWR, 0644)
require.Nil(t, err)
_, err = f.Write([]byte("test"))
require.Nil(t, err)
err = f.Close()
require.Nil(t, err)
toFS, err := clonePatchesToTemporaryFS(fromFS)
require.Nil(t, err)
f, err = toFS.OpenFile("PATCHES/test", os.O_RDONLY, 0644)
require.Nil(t, err)
b, err := io.ReadAll(f)
require.Nil(t, err)
require.Equal(t, "test", string(b))
}
// todo(mustafa): currently repositories that only has ONE commit cannot be reset.
// todo(mustafa): add support for resetting repositories with one commit and add tests.
func TestResetRepoToPoint_TwoCommits(t *testing.T) {
s, err := srpm_import.FromFile("testdata/efi-rpm-macros-3-3.el8.src.rpm", false)
require.Nil(t, err)
tempDir, err := os.MkdirTemp("", "peridot-srpm-import-test-*")
require.Nil(t, err)
// Create a bare repo in tempDir
osfsTemp := osfs.New(tempDir)
dot, err := osfsTemp.Chroot(".git")
require.Nil(t, err)
filesystemTemp := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
require.Nil(t, filesystemTemp.Init())
_, err = git.Init(filesystemTemp, nil)
require.Nil(t, err)
opts := &git.CloneOptions{
URL: tempDir,
}
storer := memory.NewStorage()
fs := memfs.New()
lookaside := storage_memory.New(osfs.New("/"))
firstImport, err := s.Import(opts, storer, fs, lookaside, "")
require.Nil(t, err)
storer2 := memory.NewStorage()
fs2 := memfs.New()
secondImport, err := s.Import(opts, storer2, fs2, lookaside, "")
require.Nil(t, err)
repo, err := getRepo("file://"+tempDir, nil)
require.Nil(t, err)
// Get wt and checkout the correct branch
wt, err := repo.Worktree()
require.Nil(t, err)
err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(secondImport.Branch),
Force: true,
})
require.Nil(t, err)
err = resetRepoToPoint(
repo,
&forge.Authenticator{AuthorName: "test", AuthorEmail: "test@rockylinux.org"},
secondImport.Commit.Hash.String(),
)
require.Nil(t, err)
// Check that only the first commit exists
log, err := repo.Log(&git.LogOptions{})
require.Nil(t, err)
commit, err := log.Next()
require.Nil(t, err)
require.NotNil(t, commit)
require.Equal(t, firstImport.Commit.Hash.String(), commit.Hash.String())
commit, err = log.Next()
require.NotNil(t, err)
require.Equal(t, "EOF", err.Error())
require.Nil(t, commit)
}
func TestResetRepoToPoint_TwoCommits_CommitAfterRetractPoint(t *testing.T) {
s, err := srpm_import.FromFile("testdata/efi-rpm-macros-3-3.el8.src.rpm", false)
require.Nil(t, err)
tempDir, err := os.MkdirTemp("", "peridot-srpm-import-test-*")
require.Nil(t, err)
// Create a bare repo in tempDir
osfsTemp := osfs.New(tempDir)
dot, err := osfsTemp.Chroot(".git")
require.Nil(t, err)
filesystemTemp := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
require.Nil(t, filesystemTemp.Init())
_, err = git.Init(filesystemTemp, nil)
require.Nil(t, err)
opts := &git.CloneOptions{
URL: tempDir,
}
storer := memory.NewStorage()
fs := memfs.New()
lookaside := storage_memory.New(osfs.New("/"))
firstImport, err := s.Import(opts, storer, fs, lookaside, "")
require.Nil(t, err)
storer2 := memory.NewStorage()
fs2 := memfs.New()
secondImport, err := s.Import(opts, storer2, fs2, lookaside, "")
require.Nil(t, err)
repo, err := getRepo("file://"+tempDir, nil)
require.Nil(t, err)
// Get wt and checkout the correct branch
wt, err := repo.Worktree()
require.Nil(t, err)
err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(secondImport.Branch),
Force: true,
})
require.Nil(t, err)
// Create a commit after the commit we want to reset to
f, err := wt.Filesystem.Create("PATCHES/test.cfg")
require.Nil(t, err)
_, err = f.Write([]byte("lookaside: { file: \"test.png\" }"))
require.Nil(t, err)
err = f.Close()
require.Nil(t, err)
_, err = wt.Add("PATCHES/test.cfg")
require.Nil(t, err)
stableTime := time.Now()
_, err = wt.Commit("test commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Mustafa Gezen",
Email: "mustafa@rockylinux.org",
When: stableTime,
},
})
require.Nil(t, err)
err = resetRepoToPoint(
repo,
&forge.Authenticator{AuthorName: "test", AuthorEmail: "test@rockylinux.org"},
secondImport.Commit.Hash.String(),
)
require.Nil(t, err)
// Check that only the first commit exists
log, err := repo.Log(&git.LogOptions{})
require.Nil(t, err)
commit, err := log.Next()
require.Nil(t, err)
require.NotNil(t, commit)
msg := `Retract "import efi-rpm-macros-3-3.el8"
Fast-forwarded following commits:
test commit
Co-authored-by: Mustafa Gezen <mustafa@rockylinux.org>
`
require.Equal(t, msg, commit.Message)
commit, err = log.Next()
require.Nil(t, err)
require.Equal(t, firstImport.Commit.Hash.String(), commit.Hash.String())
commit, err = log.Next()
require.NotNil(t, err)
require.Equal(t, "EOF", err.Error())
require.Nil(t, commit)
}
func TestWorker_RetractEntry(t *testing.T) {
s, err := srpm_import.FromFile("testdata/efi-rpm-macros-3-3.el8.src.rpm", false)
require.Nil(t, err)
tempDir := filepath.Join(tempDirForge, "efi-rpm-macros")
err = os.RemoveAll(tempDir)
require.Nil(t, err)
err = os.MkdirAll(tempDir, 0755)
require.Nil(t, err)
// Create a bare repo in tempDir
osfsTemp := osfs.New(tempDir)
dot, err := osfsTemp.Chroot(".git")
require.Nil(t, err)
filesystemTemp := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
require.Nil(t, filesystemTemp.Init())
_, err = git.Init(filesystemTemp, nil)
require.Nil(t, err)
opts := &git.CloneOptions{
URL: tempDir,
}
storer := memory.NewStorage()
fs := memfs.New()
lookaside := storage_memory.New(osfs.New("/"))
_, err = s.Import(opts, storer, fs, lookaside, "Rocky Linux release 8.8 (Green Obsidian)")
require.Nil(t, err)
storer2 := memory.NewStorage()
fs2 := memfs.New()
secondImport, err := s.Import(opts, storer2, fs2, lookaside, "Rocky Linux release 8.8 (Green Obsidian)")
require.Nil(t, err)
// Create entry
entry := &mothership_db.Entry{
Name: base.NameGen("entries"),
EntryID: "efi-rpm-macros-3-3.el8.src",
CreateTime: time.Now(),
OSRelease: "Rocky Linux release 8.8 (Green Obsidian)",
Sha256Sum: "518a9418fec1deaeb4c636615d8d81fb60146883c431ea15ab1127893d075d28",
RepositoryName: "BaseOS",
WorkerID: sql.NullString{
Valid: true,
String: "test-worker",
},
CommitURI: "file://" + tempDir,
CommitHash: secondImport.Commit.Hash.String(),
CommitBranch: "el-8.8",
CommitTag: "imports/el-8.8/efi-rpm-macros-3-3.el8",
State: mothershippb.Entry_ARCHIVED,
PackageName: "efi-rpm-macros",
}
require.Nil(t, base.Q[mothership_db.Entry](testW.db).Create(entry))
// Retract entry
res, err := testW.RetractEntry(entry.Name)
require.Nil(t, err)
require.NotNil(t, res)
require.Equal(t, entry.Name, res.Name)
}

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.
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "srpm_import",
srcs = [
"srpm_import.go",
"srpmproc_compat.go",
],
importpath = "go.resf.org/peridot/tools/mothership/worker_server/srpm_import",
visibility = ["//visibility:public"],
deps = [
"//base/go/storage",
"//vendor/github.com/go-git/go-billy/v5:go-billy",
"//vendor/github.com/go-git/go-git/v5:go-git",
"//vendor/github.com/go-git/go-git/v5/config",
"//vendor/github.com/go-git/go-git/v5/plumbing",
"//vendor/github.com/go-git/go-git/v5/plumbing/object",
"//vendor/github.com/go-git/go-git/v5/storage",
"//vendor/github.com/pkg/errors",
"//vendor/github.com/rocky-linux/srpmproc/pb",
"//vendor/github.com/rocky-linux/srpmproc/pkg/data",
"//vendor/github.com/rocky-linux/srpmproc/pkg/directives",
"//vendor/github.com/rocky-linux/srpmproc/pkg/misc",
"//vendor/github.com/sassoftware/go-rpmutils",
"//vendor/golang.org/x/crypto/openpgp",
"@org_golang_google_protobuf//encoding/prototext",
],
)
go_test(
name = "srpm_import_test",
size = "small",
srcs = [
"srpm_import_test.go",
"srpmproc_compat_test.go",
],
data = glob(["testdata/**"]),
embed = [":srpm_import"],
deps = [
"//base/go/storage/memory",
"//vendor/github.com/go-git/go-billy/v5/memfs",
"//vendor/github.com/go-git/go-billy/v5/osfs",
"//vendor/github.com/go-git/go-git/v5:go-git",
"//vendor/github.com/go-git/go-git/v5/config",
"//vendor/github.com/go-git/go-git/v5/plumbing/cache",
"//vendor/github.com/go-git/go-git/v5/plumbing/object",
"//vendor/github.com/go-git/go-git/v5/storage/filesystem",
"//vendor/github.com/go-git/go-git/v5/storage/memory",
"//vendor/github.com/rocky-linux/srpmproc/pkg/data",
"//vendor/github.com/stretchr/testify/require",
"//vendor/golang.org/x/crypto/openpgp",
],
)

View File

@ -1,778 +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 srpm_import
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
storage2 "github.com/go-git/go-git/v5/storage"
"github.com/pkg/errors"
srpmprocpb "github.com/rocky-linux/srpmproc/pb"
"github.com/rocky-linux/srpmproc/pkg/data"
"github.com/rocky-linux/srpmproc/pkg/directives"
"github.com/sassoftware/go-rpmutils"
"go.resf.org/peridot/base/go/storage"
"golang.org/x/crypto/openpgp"
"google.golang.org/protobuf/encoding/prototext"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
var (
elDistRegex = regexp.MustCompile(`el\d+`)
releaseRegex = regexp.MustCompile(`.*release (\d+\.\d+).*`)
)
type State struct {
// tempDir is the temporary directory where the SRPM is extracted to.
tempDir string
// rpm is the SRPM.
rpm *rpmutils.Rpm
// authorName is the name of the author of the commit.
authorName string
// authorEmail is the email of the author of the commit.
authorEmail string
// lookasideBlobs is a map of blob names to their SHA256 hashes.
lookasideBlobs map[string]string
// rolling determines how the branch is named.
// if true, the branch is named "elX" where X is the major release
// if false, the branch is named "el-X.Y" where X.Y is the full release
rolling bool
}
type ImportOutput struct {
// Commit is the commit object
Commit *object.Commit
// Branch is the branch name
Branch string
// Tag is the tag name
Tag string
}
// copyFromOS copies specified file from OS filesystem to target filesystem.
func copyFromOS(targetFS billy.Filesystem, path string, targetPath string) error {
// Open file from OS filesystem.
f, err := os.Open(path)
if err != nil {
return errors.Wrap(err, "failed to open file")
}
defer f.Close()
// Create file in target filesystem.
targetFile, err := targetFS.Create(targetPath)
if err != nil {
return errors.Wrap(err, "failed to create file")
}
defer targetFile.Close()
// Copy contents of file from OS filesystem to target filesystem.
_, err = io.Copy(targetFile, f)
if err != nil {
return errors.Wrap(err, "failed to copy file")
}
return nil
}
// FromFile creates a new State from an SRPM file.
// The SRPM file is extracted to a temporary directory.
func FromFile(path string, rolling bool, keys ...*openpgp.Entity) (*State, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "failed to open file")
}
defer f.Close()
// If keys is not empty, then verify the RPM signature.
if len(keys) > 0 {
_, _, err := rpmutils.Verify(f, keys)
if err != nil {
return nil, errors.Wrap(err, "failed to verify RPM")
}
// After verifying the RPM, seek back to the start of the file.
_, err = f.Seek(0, io.SeekStart)
if err != nil {
return nil, errors.Wrap(err, "failed to seek to start of file")
}
}
rpm, err := rpmutils.ReadRpm(f)
if err != nil {
return nil, errors.Wrap(err, "failed to read RPM")
}
state := &State{
rpm: rpm,
authorName: "Mship Bot",
authorEmail: "no-reply+mshipbot@resf.org",
lookasideBlobs: make(map[string]string),
rolling: rolling,
}
// Create a temporary directory.
state.tempDir, err = os.MkdirTemp("", "srpm_import-*")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
// Extract the SRPM.
err = rpm.ExpandPayload(state.tempDir)
if err != nil {
return nil, errors.Wrap(err, "failed to extract SRPM")
}
return state, nil
}
func (s *State) Close() error {
return os.RemoveAll(s.tempDir)
}
func (s *State) GetDir() string {
return s.tempDir
}
func (s *State) SetAuthor(name, email string) {
s.authorName = name
s.authorEmail = email
}
// determineLookasideBlobs determines which blobs need to be uploaded to the
// lookaside cache.
// Currently, the rule is that if a file is larger than 5MB, and is binary,
// then it should be uploaded to the lookaside cache.
// If the file name contains ".tar", then it is assumed to be a tarball, and
// is ALWAYS uploaded to the lookaside cache.
func (s *State) determineLookasideBlobs() error {
// Read all files in tempDir, except for the SPEC file
// For each file, if it is larger than 5MB, and is binary, then add it to
// the lookasideBlobs map.
// If the file is not binary, then skip it.
ls, err := os.ReadDir(s.tempDir)
if err != nil {
return errors.Wrap(err, "failed to read directory")
}
for _, f := range ls {
// If file ends with ".spec", then skip it.
if f.IsDir() || strings.HasSuffix(f.Name(), ".spec") {
continue
}
// If file is larger than 5MB, then add it to the lookasideBlobs map.
info, err := f.Info()
if err != nil {
return errors.Wrap(err, "failed to get file info")
}
if info.Size() > 5*1024*1024 || strings.Contains(f.Name(), ".tar") {
sum, err := func() (string, error) {
hash := sha256.New()
file, err := os.Open(filepath.Join(s.tempDir, f.Name()))
if err != nil {
return "", errors.Wrap(err, "failed to open file")
}
defer file.Close()
_, err = io.Copy(hash, file)
if err != nil {
return "", errors.Wrap(err, "failed to copy file")
}
return hex.EncodeToString(hash.Sum(nil)), nil
}()
if err != nil {
return err
}
s.lookasideBlobs[f.Name()] = sum
}
}
return nil
}
// uploadLookasideBlobs uploads all blobs in the lookasideBlobs map to the
// lookaside cache.
func (s *State) uploadLookasideBlobs(lookaside storage.Storage) error {
// The object name is the SHA256 hash of the file.
for path, hash := range s.lookasideBlobs {
// First check if they exist, since it's a waste of time to upload
// something that already exists.
// They are uploaded by hash, so if the hash already exists, then the
// file already exists.
exists, err := lookaside.Exists(hash)
if err != nil {
return errors.Wrap(err, "failed to check if blob exists")
}
if exists {
continue
}
_, err = lookaside.Put(hash, filepath.Join(s.tempDir, path))
if err != nil {
return errors.Wrap(err, "failed to upload file")
}
}
return nil
}
// writeMetadata file writes the metadata map file.
// The metadata file contains lines of the format:
//
// <path to download> <blob hash>
//
// For example:
//
// 1234567890abcdef SOURCES/bar
func (s *State) writeMetadataFile(targetFS billy.Filesystem) error {
// Open metadata file for writing.
name, err := s.rpm.Header.GetStrings(rpmutils.NAME)
if err != nil {
return errors.Wrap(err, "failed to get RPM name")
}
metadataFile := fmt.Sprintf(".%s.metadata", name[0])
// Delete the file if it exists
_ = targetFS.Remove(metadataFile)
f, err := targetFS.Create(metadataFile)
if err != nil {
return errors.Wrap(err, "failed to open metadata file")
}
defer f.Close()
// Write each line to the metadata file.
for path, hash := range s.lookasideBlobs {
// RPM sources MUST be in SOURCES/ directory
_, err = f.Write([]byte(hash + " " + filepath.Join("SOURCES", path) + "\n"))
if err != nil {
return errors.Wrap(err, "failed to write line to metadata file")
}
}
// Each file in metadata needs to be added to gitignore
// Overwrite the gitignore file
gitignoreFile := ".gitignore"
f, err = targetFS.OpenFile(gitignoreFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.Wrap(err, "failed to open gitignore file")
}
// Write each line to the gitignore file.
for path, _ := range s.lookasideBlobs {
_, err = f.Write([]byte(filepath.Join("SOURCES", path) + "\n"))
if err != nil {
return errors.Wrap(err, "failed to write line to gitignore file")
}
}
return nil
}
// expandLayout expands the layout of the SRPM into the target filesystem.
// Moves all sources into SOURCES/ directory.
// Spec file is moved to SPECS/ directory.
func (s *State) expandLayout(targetFS billy.Filesystem) error {
// Create SOURCES/ directory.
err := targetFS.MkdirAll("SOURCES", 0755)
if err != nil {
return errors.Wrap(err, "failed to create SOURCES directory")
}
// Copy all files from OS filesystem to target filesystem.
ls, err := os.ReadDir(s.tempDir)
if err != nil {
return errors.Wrap(err, "failed to read directory")
}
for _, f := range ls {
baseName := filepath.Base(f.Name())
// If file ends with ".spec", then copy to SPECS/ directory.
if strings.HasSuffix(f.Name(), ".spec") {
err := copyFromOS(targetFS, filepath.Join(s.tempDir, f.Name()), filepath.Join("SPECS", baseName))
if err != nil {
return errors.Wrap(err, "failed to copy spec file")
}
} else {
// Copy all other files to SOURCES/ directory.
// Only if they are not present in lookasideBlobs
_, ok := s.lookasideBlobs[f.Name()]
if ok {
continue
}
err := copyFromOS(targetFS, filepath.Join(s.tempDir, f.Name()), filepath.Join("SOURCES", baseName))
if err != nil {
return errors.Wrap(err, "failed to copy file")
}
}
}
return nil
}
// getStreamSuffix adds a "-stream-X" suffix if the given RPM is a module component.
// This is determined using Modularitylabel (5096). If the label is present, then
// the RPM is a module component. Label format is MODULE_NAME:STREAM:VERSION:CONTEXT.
// This function returns an empty string if the RPM is not a module component.
func (s *State) getStreamSuffix() (string, error) {
// Check the modularity label
label, err := s.rpm.Header.GetString(5096)
if err != nil {
// If it's not present at all, it will fail with "No such entry 5096"
return "", nil
}
// If the label is empty, then the RPM is not a module component
if label == "" {
return "", nil
}
// Split the label
parts := strings.Split(label, ":")
if len(parts) != 4 {
return "", fmt.Errorf("invalid modularity label")
}
// Return the stream
return fmt.Sprintf("-stream-%s", parts[1]), nil
}
// getRepo returns the target repository for the SRPM.
// This is where the payload is uploaded to.
func (s *State) getRepo(opts *git.CloneOptions, storer storage2.Storer, targetFS billy.Filesystem, osRelease string) (*git.Repository, string, error) {
// Determine branch
// If the OS release is not specified, then we use the dist tag
var branch string
if osRelease == "" {
// Determine dist tag
nevra, err := s.rpm.Header.GetNEVRA()
if err != nil {
return nil, "", errors.Wrap(err, "failed to get NEVRA")
}
// The dist tag will be used as the branch
dist := elDistRegex.FindString(nevra.Release)
if dist == "" {
return nil, "", errors.Wrap(err, "failed to determine dist tag")
}
if s.rolling {
branch = dist
} else {
branch = "el-" + dist[2:]
}
} else {
// Determine branch from OS release
if !releaseRegex.MatchString(osRelease) {
return nil, "", fmt.Errorf("invalid OS release %s", osRelease)
}
ver := releaseRegex.FindStringSubmatch(osRelease)[1]
if s.rolling {
dist := elDistRegex.FindString("el" + ver)
if dist == "" {
return nil, "", errors.New("failed to determine dist tag")
}
branch = dist
} else {
branch = "el-" + ver
}
}
// Check if module component
streamSuffix, err := s.getStreamSuffix()
if err != nil {
return nil, "", errors.Wrap(err, "failed to get stream suffix")
}
branch += streamSuffix
// Set branch to dist tag
opts.ReferenceName = plumbing.NewBranchReferenceName(branch)
opts.SingleBranch = true
// Clone the repository, to the target filesystem.
// We do an init, then a fetch, then a checkout
// If the repo doesn't exist, then we init only
repo, err := git.Init(storer, targetFS)
if err != nil {
return nil, "", errors.Wrap(err, "failed to init repo")
}
wt, err := repo.Worktree()
if err != nil {
return nil, "", errors.Wrap(err, "failed to get worktree")
}
// Create a new remote
_, err = repo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{opts.URL},
Fetch: []config.RefSpec{
config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%[1]s", branch)),
},
})
if err != nil {
return nil, "", errors.Wrap(err, "failed to create remote")
}
// Fetch the remote
err = repo.Fetch(&git.FetchOptions{
Auth: opts.Auth,
RemoteName: "origin",
RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%[1]s", branch)),
},
})
// Checkout the branch
refName := plumbing.NewBranchReferenceName(branch)
if err != nil {
h := plumbing.NewSymbolicReference(plumbing.HEAD, refName)
if err := repo.Storer.CheckAndSetReference(h, nil); err != nil {
return nil, "", errors.Wrap(err, "failed to checkout branch")
}
} else {
err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch),
Force: true,
})
}
return repo, branch, nil
}
// cleanTargetRepo deletes all files in the target repository.
func (s *State) cleanTargetRepo(wt *git.Worktree, root string) error {
// Delete all files in the target repository.
ls, err := wt.Filesystem.ReadDir(root)
if err != nil {
return errors.Wrap(err, "failed to read directory")
}
for _, f := range ls {
// Don't delete the PATCHES directory
if f.Name() == "PATCHES" && f.IsDir() {
continue
}
// If it's a directory, then recurse into it.
if f.IsDir() {
err := s.cleanTargetRepo(wt, filepath.Join(root, f.Name()))
if err != nil {
return errors.Wrap(err, "failed to clean target repo")
}
} else {
// Otherwise, delete the file.
_, err := wt.Remove(filepath.Join(root, f.Name()))
if err != nil {
return errors.Wrap(err, "failed to remove file")
}
}
}
return nil
}
// populateTargetRepo runs the following steps:
// 1. Clean the target repository.
// 2. Determine which blobs need to be uploaded to the lookaside cache.
// 3. Upload blobs to the lookaside cache.
// 4. Write the metadata file.
// 5. Expand the layout of the SRPM.
// 6. Commit the changes to the target repository.
func (s *State) populateTargetRepo(repo *git.Repository, targetFS billy.Filesystem, lookaside storage.Storage, branch string) error {
// Clean the target repository.
wt, err := repo.Worktree()
if err != nil {
return errors.Wrap(err, "failed to get worktree")
}
err = s.cleanTargetRepo(wt, ".")
if err != nil {
return errors.Wrap(err, "failed to clean target repo")
}
// Determine which blobs need to be uploaded to the lookaside cache.
err = s.determineLookasideBlobs()
if err != nil {
return errors.Wrap(err, "failed to determine lookaside blobs")
}
// Upload blobs to the lookaside cache.
err = s.uploadLookasideBlobs(lookaside)
if err != nil {
return errors.Wrap(err, "failed to upload lookaside blobs")
}
// Write the metadata file.
err = s.writeMetadataFile(targetFS)
if err != nil {
return errors.Wrap(err, "failed to write metadata file")
}
// Expand the layout of the SRPM.
err = s.expandLayout(targetFS)
if err != nil {
return errors.Wrap(err, "failed to expand layout")
}
// If the target FS has patches, apply the directives
err = s.patchTargetRepo(repo, lookaside)
if err != nil {
return errors.Wrap(err, "failed to patch target repo")
}
// Commit the changes to the target repository.
_, err = wt.Add(".")
if err != nil {
return errors.Wrap(err, "failed to add files")
}
nevra, err := s.rpm.Header.GetNEVRA()
if err != nil {
return errors.Wrap(err, "failed to get NEVRA")
}
importStr := fmt.Sprintf("import %s-%s-%s", nevra.Name, nevra.Version, nevra.Release)
hash, err := wt.Commit(importStr, &git.CommitOptions{
Author: &object.Signature{
Name: s.authorName,
Email: s.authorEmail,
When: time.Now(),
},
AllowEmptyCommits: true,
})
if err != nil {
return errors.Wrap(err, "failed to commit changes")
}
// Create a tag
// The tag should follow the following format:
// imports/<branch>/<nvra>
tag := fmt.Sprintf("imports/%s/%s-%s-%s", branch, nevra.Name, nevra.Version, nevra.Release)
_, err = repo.CreateTag(tag, hash, &git.CreateTagOptions{
Tagger: &object.Signature{
Name: s.authorName,
Email: s.authorEmail,
When: time.Now(),
},
Message: tag,
})
if err != nil {
return errors.Wrap(err, "failed to create tag")
}
return nil
}
// pushTargetRepo pushes the target repository to the upstream repository.
func (s *State) pushTargetRepo(repo *git.Repository, opts *git.PushOptions) error {
// Push the target repository to the upstream repository.
err := repo.Push(opts)
if err != nil {
return errors.Wrap(err, "failed to push repo")
}
return nil
}
func (s *State) patchTargetRepo(repo *git.Repository, lookaside storage.Storage) error {
// We can re-use srpmproc as we should stay compatible with it
// Instead of OpenPatch, we'll look for patches in the targetFS
// todo(mustafa): RESF still uses OpenPatch, so we'll need to change that
wt, err := repo.Worktree()
if err != nil {
return errors.Wrap(err, "failed to get worktree")
}
nevra, err := s.rpm.Header.GetNEVRA()
if err != nil {
return errors.Wrap(err, "failed to get NEVRA")
}
dist := elDistRegex.FindString(nevra.Release)
if dist == "" {
return errors.Wrap(err, "failed to determine dist tag")
}
distNum, err := strconv.Atoi(dist[2:])
if err != nil {
return errors.Wrap(err, "failed to parse dist tag")
}
pd := &data.ProcessData{
ImportBranchPrefix: "el",
Version: distNum,
BlobStorage: &srpmprocBlobCompat{lookaside},
Importer: &srpmprocImportModeCompat{},
Log: log.New(os.Stderr, "", 0),
}
md := &data.ModeData{
SourcesToIgnore: []*data.IgnoredSource{},
}
// Look in the PATCHES/ directory for any .cfg files
patchesLs, err := wt.Filesystem.ReadDir("PATCHES")
if err != nil {
return errors.Wrap(err, "failed to read PATCHES directory")
}
for _, f := range patchesLs {
// Skip directories
if f.IsDir() {
continue
}
// Skip non-cfg files
if !strings.HasSuffix(f.Name(), ".cfg") {
continue
}
// Open the file
file, err := wt.Filesystem.Open(filepath.Join("PATCHES", f.Name()))
if err != nil {
return errors.Wrap(err, "failed to open file")
}
// Process the file
directivesBytes, err := io.ReadAll(file)
if err != nil {
return errors.Wrap(err, "failed to read file")
}
var cfg srpmprocpb.Cfg
err = prototext.Unmarshal(directivesBytes, &cfg)
if err != nil {
return errors.Wrap(err, "failed to unmarshal directives")
}
errs := directives.Apply(&cfg, pd, md, wt, wt)
// If there are errors, then we should return a reduced error
if len(errs) > 0 {
var retErr error
for _, err := range errs {
retErr = errors.Wrap(retErr, err.Error())
}
return retErr
}
}
// Add sources to ignore to lookasideBlobs
for _, source := range md.SourcesToIgnore {
// Get the hash of the source
hash, err := func() (string, error) {
hash := sha256.New()
file, err := wt.Filesystem.Open(source.Name)
if err != nil {
return "", errors.Wrap(err, "failed to open file")
}
defer file.Close()
_, err = io.Copy(hash, file)
if err != nil {
return "", errors.Wrap(err, "failed to copy file")
}
return hex.EncodeToString(hash.Sum(nil)), nil
}()
if err != nil {
return err
}
s.lookasideBlobs[source.Name] = hash
}
// Re-write the metadata file
err = s.writeMetadataFile(wt.Filesystem)
if err != nil {
return errors.Wrap(err, "failed to write metadata file")
}
return nil
}
// Import imports the SRPM into the target repository.
func (s *State) Import(opts *git.CloneOptions, storer storage2.Storer, targetFS billy.Filesystem, lookaside storage.Storage, osRelease string) (*ImportOutput, error) {
nevra, err := s.rpm.Header.GetNEVRA()
if err != nil {
return nil, errors.Wrap(err, "failed to get NEVRA")
}
// Get the target repository.
repo, branch, err := s.getRepo(opts, storer, targetFS, osRelease)
if err != nil {
return nil, errors.Wrap(err, "failed to get repo")
}
// Populate the target repository.
err = s.populateTargetRepo(repo, targetFS, lookaside, branch)
if err != nil {
return nil, errors.Wrap(err, "failed to populate target repo")
}
// Push the target repository.
err = s.pushTargetRepo(repo, &git.PushOptions{
Force: true,
Auth: opts.Auth,
RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%[1]s", branch)),
config.RefSpec(fmt.Sprintf("refs/tags/imports/%s/*:refs/tags/imports/%[1]s/*", branch)),
},
})
if err != nil {
return nil, errors.Wrap(err, "failed to push target repo")
}
// Get latest commit
head, err := repo.Head()
if err != nil {
return nil, errors.Wrap(err, "failed to get HEAD")
}
// Get commit object
commit, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, errors.Wrap(err, "failed to get commit object")
}
tag := fmt.Sprintf("imports/%s/%s-%s-%s", branch, nevra.Name, nevra.Version, nevra.Release)
return &ImportOutput{
Commit: commit,
Branch: branch,
Tag: tag,
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +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 srpm_import
import (
"github.com/rocky-linux/srpmproc/pkg/data"
"github.com/rocky-linux/srpmproc/pkg/misc"
"go.resf.org/peridot/base/go/storage"
"strings"
)
type srpmprocBlobCompat struct {
storage.Storage
}
func (s *srpmprocBlobCompat) Write(path string, content []byte) error {
_, err := s.PutBytes(path, content)
return err
}
func (s *srpmprocBlobCompat) Read(path string) ([]byte, error) {
return s.Get(path)
}
type srpmprocImportModeCompat struct {
data.ImportMode
}
func (s *srpmprocImportModeCompat) ImportName(pd *data.ProcessData, md *data.ModeData) string {
if misc.GetTagImportRegex(pd).MatchString(md.TagBranch) {
match := misc.GetTagImportRegex(pd).FindStringSubmatch(md.TagBranch)
return match[3]
}
return strings.Replace(strings.TrimPrefix(md.TagBranch, "refs/heads/"), "%", "_", -1)
}

View File

@ -1,57 +0,0 @@
package srpm_import
import (
"github.com/go-git/go-billy/v5/memfs"
"github.com/rocky-linux/srpmproc/pkg/data"
"github.com/stretchr/testify/require"
storage_memory "go.resf.org/peridot/base/go/storage/memory"
"testing"
)
func TestSrpmprocBlobCompat_Write(t *testing.T) {
lookaside := storage_memory.New(memfs.New())
s := &srpmprocBlobCompat{lookaside}
require.Nil(t, s.Write("test", []byte("test")))
x, err := lookaside.Get("test")
require.Nil(t, err)
require.Equal(t, []byte("test"), x)
}
func TestSrpmprocBlobCompat_Read(t *testing.T) {
lookaside := storage_memory.New(memfs.New())
s := &srpmprocBlobCompat{lookaside}
_, err := lookaside.PutBytes("test", []byte("test"))
require.Nil(t, err)
x, err := s.Read("test")
require.Nil(t, err)
require.Equal(t, []byte("test"), x)
}
func TestSrpmprocImportModeCompat_ImportName(t *testing.T) {
s := &srpmprocImportModeCompat{}
pd := &data.ProcessData{
ImportBranchPrefix: "r",
Version: 9,
RpmLocation: "bash",
}
md := &data.ModeData{
SourcesToIgnore: []*data.IgnoredSource{},
TagBranch: "refs/tags/imports/r9/bash-5.1.8-4.el9",
}
require.Equal(t, "bash-5.1.8-4.el9", s.ImportName(pd, md))
}
// todo(mustafa): actually recall what this mode was useful for in srpmproc. like what is this??
func TestSrpmprocImportModeCompat_ImportName_NoTag(t *testing.T) {
s := &srpmprocImportModeCompat{}
pd := &data.ProcessData{
ImportBranchPrefix: "el",
Version: 9,
RpmLocation: "bash",
}
md := &data.ModeData{
SourcesToIgnore: []*data.IgnoredSource{},
TagBranch: "refs/heads/el9",
}
require.Equal(t, "el9", s.ImportName(pd, md))
}

View File

@ -1,29 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGAofzYBEAC6yS1azw6f3wmaVd//3aSy6O2c9+jeetulRQvg2LvhRRS1eNqp
/x9tbBhfohu/tlDkGpYHV7diePgMml9SZDy1sKlI3tDhx6GZ3xwF0fd1vWBZpmNk
D9gRkUmYBeLotmcXQZ8ZpWLicosFtDpJEYpLUhuIgTKwt4gxJrHvkWsGQiBkJxKD
u3/RlL4IYA3Ot9iuCBflc91EyAw1Yj0gKcDzbOqjvlGtS3ASXgxPqSfU0uLC9USF
uKDnP2tcnlKKGfj0u6VkqISliSuRAzjlKho9Meond+mMIFOTT6qp4xyu+9Dj3IjZ
IC6rBXRU3xi8z0qYptoFZ6hx70NV5u+0XUzDMXdjQ5S859RYJKijiwmfMC7gZQAf
OkdOcicNzen/TwD/slhiCDssHBNEe86Wwu5kmDoCri7GJlYOlWU42Xi0o1JkVltN
D8ZId+EBDIms7ugSwGOVSxyZs43q2IAfFYCRtyKHFlgHBRe9/KTWPUrnsfKxGJgC
Do3Yb63/IYTvfTJptVfhQtL1AhEAeF1I+buVoJRmBEyYKD9BdU4xQN39VrZKziO3
hDIGng/eK6PaPhUdq6XqvmnsZ2h+KVbyoj4cTo2gKCB2XA7O2HLQsuGduHzYKNjf
QR9j0djjwTrsvGvzfEzchP19723vYf7GdcLvqtPqzpxSX2FNARpCGXBw9wARAQAB
tDNSZWxlYXNlIEVuZ2luZWVyaW5nIDxpbmZyYXN0cnVjdHVyZUByb2NreWxpbnV4
Lm9yZz6JAk4EEwEIADgWIQRwUcRwqSn0VM6+N7cVr12sbXRaYAUCYCh/NgIbDwUL
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAVr12sbXRaYLFmEACSMvoO1FDdyAbu
1m6xEzDhs7FgnZeQNzLZECv2j+ggFSJXezlNVOZ5I1I8umBan2ywfKQD8M+IjmrW
k9/7h9i54t8RS/RN7KNo7ECGnKXqXDPzBBTs1Gwo1WzltAoaDKUfXqQ4oJ4aCP/q
/XPVWEzgpJO1XEezvCq8VXisutyDiXEjjMIeBczxb1hbamQX+jLTIQ1MDJ4Zo1YP
zlUqrHW434XC2b1/WbSaylq8Wk9cksca5J+g3FqTlgiWozyy0uxygIRjb6iTzKXk
V7SYxeXp3hNTuoUgiFkjh5/0yKWCwx7aQqlHar9GjpxmBDAO0kzOlgtTw//EqTwR
KnYZLig9FW0PhwvZJUigr0cvs/XXTTb77z/i/dfHkrjVTTYenNyXogPtTtSyxqca
61fbPf0B/S3N43PW8URXBRS0sykpX4SxKu+PwKCqf+OJ7hMEVAapqzTt1q9T7zyB
QwvCVx8s7WWvXbs2d6ZUrArklgjHoHQcdxJKdhuRmD34AuXWCLW+gH8rJWZpuNl3
+WsPZX4PvjKDgMw6YMcV7zhWX6c0SevKtzt7WP3XoKDuPhK1PMGJQqQ7spegGB+5
DZvsJS48Ip0S45Qfmj82ibXaCBJHTNZE8Zs+rdTjQ9DS5qvzRA1sRA1dBb/7OLYE
JmeWf4VZyebm+gc50szsg6Ut2yT8hw==
=AiP8
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,31 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: resf.keykeeper.v1
Comment: Keykeeper
xsFNBGJ5RksBEADF/Lzssm7uryV6+VHAgL36klyCVcHwvx9Bk853LBOuHVEZWsme
kbJF3fQG7i7gfCKGuV5XW15xINToe4fBThZteGJziboSZRpkEQ2z3lYcbg34X7+d
co833lkBNgz1v6QO7PmAdY/x76Q6Hx0J9yiJWd+4j+vRi4hbWuh64vUtTd7rPwk8
0y3g4oK1YT0NR0Xm/QUO9vWmkSTVflQ6y82HhHIUrG+1vQnSOrWaC0O1lqUI3Nuo
b6jTARCmbaPsi+XVQnBbsnPPq6Tblwc+NYJSqj5d9nT0uEXT7Zovj4Je5oWVFXp9
P1OWkbo2z5XkKjoeobM/zKDESJR78h+YQAN9IOKFjL/u/Gzrk1oEgByCABXOX+H5
hfucrq5U3bbcKy4e5tYgnnZxqpELv3fN/2l8iZknHEh5aYNT5WXVHpD/8u2rMmwm
I9YTEMueEtmVy0ZV3opUzOlC+3ZUwjmvAJtdfJyeVW/VMy3Hw3Ih0Fij91rO613V
7n72ggVlJiX25jYyT4AXlaGfAOMndJNVgBps0RArOBYsJRPnvfHlLi5cfjVd7vYx
QhGX9ODYuvyJ/rW70dMVikeSjlBDKS08tvdqOgtiYy4yhtY4ijQC9BmCE9H9gOxU
FN297iLimAxr0EVsED96fP96TbDGILWsfJuxAvoqmpkElv8J+P1/F7to2QARAQAB
zU9Sb2NreSBFbnRlcnByaXNlIFNvZnR3YXJlIEZvdW5kYXRpb24gLSBSZWxlYXNl
IGtleSAyMDIyIDxyZWxlbmdAcm9ja3lsaW51eC5vcmc+wsGKBBMBCAA0BQJieUZL
FiEEIcslauFvxUxuZSlJcC1CbTUNJ10CGwMCHgECGQEDCwkHAhUIAxYAAgIiAQAK
CRBwLUJtNQ0nXWQ5D/9472seOyRO6//bQ2ns3w9lE+aTLlJ5CY0GSTb4xNuyv+AD
IXpgvLSMtTR0fp9GV3vMw6QIWsehDqt7O5xKWi+3tYdaXRpb1cvnh8r/oCcvI4uL
k8kImNgsx+Cj+drKeQo03vFxBTDi1BTQFkfEt32fA2Aw5gYcGElM717sNMAMQFEH
P+OW5hYDH4kcLbtUypPXFbcXUbaf6jUjfiEp5lLjqquzAyDPLlkzMr5RVa9n3/rI
R6OQp5loPVzCRZMgDLALBU2TcFXLVP+6hAW8qM77c+q/rOysP+Yd+N7GAd0fvEvA
mfeA4Y6dP0mMRu96EEAJ1qSKFWUul6K6nuqy+JTxktpw8F/IBAz44na17Tf02MJH
GCUWyM0n5vuO5kK+Ykkkwd+v43ZlqDnwG7akDkLwgj6O0QNx2TGkdgt3+C6aHN5S
MiF0pi0qYbiN9LO0e05Ai2r3zTFC/pCaBWlG1ph2jx1pDy4yUVPfswWFNfe5I+4i
CMHPRFsZNYxQnIA2Prtgt2YMwz3VIGI6DT/Z56Joqw4eOfaJTTQSXCANts/gD7qW
D3SZXPc7wQD63TpDEjJdqhmepaTECbxN7x/p+GwIZYWJN+AYhvrfGXfjud3eDu8/
i+YIbPKH1TAOMwiyxC106mIL705p+ORf5zATZMyB8Y0OvRIz5aKkBDFZM2QN6A==
=PzIf
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,29 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGAofzYBEAC6yS1azw6f3wmaVd//3aSy6O2c9+jeetulRQvg2LvhRRS1eNqp
/x9tbBhfohu/tlDkGpYHV7diePgMml9SZDy1sKlI3tDhx6GZ3xwF0fd1vWBZpmNk
D9gRkUmYBeLotmcXQZ8ZpWLicosFtDpJEYpLUhuIgTKwt4gxJrHvkWsGQiBkJxKD
u3/RlL4IYA3Ot9iuCBflc91EyAw1Yj0gKcDzbOqjvlGtS3ASXgxPqSfU0uLC9USF
uKDnP2tcnlKKGfj0u6VkqISliSuRAzjlKho9Meond+mMIFOTT6qp4xyu+9Dj3IjZ
IC6rBXRU3xi8z0qYptoFZ6hx70NV5u+0XUzDMXdjQ5S859RYJKijiwmfMC7gZQAf
OkdOcicNzen/TwD/slhiCDssHBNEe86Wwu5kmDoCri7GJlYOlWU42Xi0o1JkVltN
D8ZId+EBDIms7ugSwGOVSxyZs43q2IAfFYCRtyKHFlgHBRe9/KTWPUrnsfKxGJgC
Do3Yb63/IYTvfTJptVfhQtL1AhEAeF1I+buVoJRmBEyYKD9BdU4xQN39VrZKziO3
hDIGng/eK6PaPhUdq6XqvmnsZ2h+KVbyoj4cTo2gKCB2XA7O2HLQsuGduHzYKNjf
QR9j0djjwTrsvGvzfEzchP19723vYf7GdcLvqtPqzpxSX2FNARpCGXBw9wARAQAB
tDNSZWxlYXNlIEVuZ2luZWVyaW5nIDxpbmZyYXN0cnVjdHVyZUByb2NreWxpbnV4
Lm9yZz6JAk4EEwEIADgWIQRwUcRwqSn0VM6+N7cVr12sbXRaYAUCYCh/NgIbDwUL
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAVr12sbXRaYLFmEACSMvoO1FDdyAbu
1m6xEzDhs7FgnZeQNzLZECv2j+ggFSJXezlNVOZ5I1I8umBan2ywfKQD8M+IjmrW
k9/7h9i54t8RS/RN7KNo7ECGnKXqXDPzBBTs1Gwo1WzltAoaDKUfXqQ4oJ4aCP/q
/XPVWEzgpJO1XEezvCq8VXisutyDiXEjjMIeBczxb1hbamQX+jLTIQ1MDJ4Zo1YP
zlUqrHW434XC2b1/WbSaylq8Wk9cksca5J+g3FqTlgiWozyy0uxygIRjb6iTzKXk
V7SYxeXp3hNTuoUgiFkjh5/0yKWCwx7aQqlHar9GjpxmBDAO0kzOlgtTw//EqTwR
KnYZLig9FW0PhwvZJUigr0cvs/XXTTb77z/i/dfHkrjVTTYenNyXogPtTtSyxqca
61fbPf0B/S3N43PW8URXBRS0sykpX4SxKu+PwKCqf+OJ7hMEVAapqzTt1q9T7zyB
QwvCVx8s7WWvXbs2d6ZUrArklgjHoHQcdxJKdhuRmD34AuXWCLW+gH8rJWZpuNl3
+WsPZX4PvjKDgMw6YMcV7zhWX6c0SevKtzt7WP3XoKDuPhK1PMGJQqQ7spegGB+5
DZvsJS48Ip0S45Qfmj82ibXaCBJHTNZE8Zs+rdTjQ9DS5qvzRA1sRA1dBb/7OLYE
JmeWf4VZyebm+gc50szsg6Ut2yT8hw==
=AiP8
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,32 +0,0 @@
package mothership_worker_server
import (
"github.com/pkg/errors"
"go.temporal.io/sdk/temporal"
"net/url"
"strings"
)
func getObjectPath(uri string) (string, error) {
// Get object name from URI.
// Check if object exists.
// If not, return error.
parsed, err := url.Parse(uri)
if err != nil {
return "", temporal.NewNonRetryableApplicationError(
"could not parse resource URI",
"couldNotParseResourceURI",
errors.Wrap(err, "failed to parse resource URI"),
)
}
// S3 for example must include bucket, while memory:// does not.
// So memory://test.rpm would be parsed as host=test.rpm, path="".
// While s3://mship/test.rpm would be parsed as host=mship, path=test.rpm.
object := strings.TrimPrefix(parsed.Path, "/")
if object == "" {
object = parsed.Host
}
return object, nil
}

View File

@ -1,23 +0,0 @@
package mothership_worker_server
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestGetObjectPath_Path_S3(t *testing.T) {
object, err := getObjectPath("s3://mship/test.rpm")
require.Nil(t, err)
require.Equal(t, "test.rpm", object)
}
func TestGetObjectPath_Host_Memory(t *testing.T) {
object, err := getObjectPath("memory://test.rpm")
require.Nil(t, err)
require.Equal(t, "test.rpm", object)
}
func TestGetObjectPath_InvalidURI(t *testing.T) {
_, err := getObjectPath("test://test:test/")
require.NotNil(t, err)
}

View File

@ -1,40 +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 mothership_worker_server
import (
base "go.resf.org/peridot/base/go"
"go.resf.org/peridot/base/go/forge"
"go.resf.org/peridot/base/go/storage"
"golang.org/x/crypto/openpgp"
)
type Worker struct {
db *base.DB
storage storage.Storage
gpgKeys openpgp.EntityList
forge forge.Forge
rolling bool
}
func New(db *base.DB, storage storage.Storage, gpgKeys openpgp.EntityList, forge forge.Forge, rolling bool) *Worker {
return &Worker{
db: db,
storage: storage,
gpgKeys: gpgKeys,
forge: forge,
rolling: rolling,
}
}

View File

@ -1,258 +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 mothership_worker_server
import (
mshipadminpb "go.resf.org/peridot/tools/mothership/admin/pb"
mothershippb "go.resf.org/peridot/tools/mothership/pb"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
"time"
)
var w Worker
// processRPMPostHold is a part of the ProcessRPM workflow.
// This part executes the import part, and retries if it fails.
// After the first failure, the workflow is put on hold.
// If the workflow is put on hold, the workflow can be rescued by an admin.
func processRPMPostHold(ctx workflow.Context, entry *mothershippb.Entry, args *mothershippb.ProcessRPMArgs) (*mothershippb.ProcessRPMResponse, error) {
// If resource exists, then we can start the import.
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
// We'll wait up to 5 minutes for the import to finish.
// Most imports are fast, but some packages are very large.
StartToCloseTimeout: 5 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 1,
},
})
var importRpmRes mothershippb.ImportRPMResponse
err := workflow.ExecuteActivity(ctx, w.ImportRPM, args.Request.RpmUri, args.Request.Checksum, args.Request.OsRelease).Get(ctx, &importRpmRes)
if err != nil {
// If the import fails, we'll put the workflow on hold.
// If the workflow is put on hold, an admin can rescue the workflow.
var err error
signalChan := workflow.GetSignalChannel(ctx, "rescue")
workflow.GetLogger(ctx).Info("Import failed, putting workflow on hold")
selector := workflow.NewSelector(ctx)
selector.AddReceive(ctx.Done(), func(c workflow.ReceiveChannel, more bool) {
err = ctx.Err()
})
selector.AddReceive(signalChan, func(c workflow.ReceiveChannel, more bool) {
c.Receive(ctx, nil)
err = nil
})
// Set state to on hold
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 0,
},
})
err = workflow.ExecuteActivity(ctx, w.SetEntryState, entry.Name, mothershippb.Entry_ON_HOLD, nil).Get(ctx, entry)
if err != nil {
return nil, err
}
// Wait until a rescue signal is received. Otherwise, an admin can also
// cancel the workflow.
selector.Select(ctx)
// Check if workflow was cancelled.
if err != nil {
ctx, cancel := workflow.NewDisconnectedContext(ctx)
defer cancel()
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 0,
},
})
_ = workflow.ExecuteActivity(ctx, w.SetEntryState, entry.Name, mothershippb.Entry_CANCELLED, nil).Get(ctx, entry)
return nil, err
}
// Set the entry state to archiving
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 0,
},
})
err = workflow.ExecuteActivity(ctx, w.SetEntryState, entry.Name, mothershippb.Entry_ARCHIVING, nil).Get(ctx, entry)
if err != nil {
return nil, err
}
// If the workflow was not cancelled, then we can retry the import.
return processRPMPostHold(ctx, entry, args)
}
// If the import succeeds, then we can update the entry state.
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 0,
},
})
err = workflow.ExecuteActivity(ctx, w.SetEntryState, entry.Name, mothershippb.Entry_ARCHIVED, &importRpmRes).Get(ctx, entry)
if err != nil {
return nil, err
}
return &mothershippb.ProcessRPMResponse{
Entry: entry,
}, nil
}
// ProcessRPMWorkflow processes an SRPM.
// Usually a client worker will first initiate an upload to the storage backend,
// then send a request to the Server `SubmitEntry` method (or send a request
// then upload the resource).
func ProcessRPMWorkflow(ctx workflow.Context, args *mothershippb.ProcessRPMArgs) (*mothershippb.ProcessRPMResponse, error) {
// First verify that the resource exists.
// The resource can be uploaded after the request is sent.
// So we should wait up to 2 hours. The initial timeouts should be low
// since the worker is most likely to upload the resource immediately.
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
// We're waiting 25 seconds each time
InitialInterval: 25 * time.Second,
BackoffCoefficient: 1,
// Maximum attempts should be set, so it's approximately 2 hours
MaximumAttempts: (60 * 60 * 2) / 25,
},
})
err := workflow.ExecuteActivity(ctx, w.VerifyResourceExists, args.Request.RpmUri).Get(ctx, nil)
if err != nil {
return nil, err
}
// Set worker last check in time
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
})
err = workflow.ExecuteActivity(ctx, w.SetWorkerLastCheckinTime, args.InternalRequest.WorkerId).Get(ctx, nil)
if err != nil {
return nil, err
}
// Create an entry, if the import fails, we'll still have an entry.
// If it succeeds, we'll update the entry state.
// If it fails we can set the workflow on hold and if the patches are updated
// an admin can signal and "rescue" the workflow.
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
})
var entry mothershippb.Entry
err = workflow.ExecuteActivity(ctx, w.CreateEntry, args).Get(ctx, &entry)
if err != nil {
return nil, err
}
// On defer, if the workflow is not completed, then we'll set the entry state
// to failed.
defer func() {
if entry.State == mothershippb.Entry_ARCHIVED || entry.State == mothershippb.Entry_CANCELLED {
return
}
ctx, cancel := workflow.NewDisconnectedContext(ctx)
defer cancel()
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 0,
},
})
// Check if entry has EntryID set, if not then we can just delete the entry
if entry.EntryId == "" {
_ = workflow.ExecuteActivity(ctx, w.DeleteEntry, entry.Name).Get(ctx, nil)
return
}
_ = workflow.ExecuteActivity(ctx, w.SetEntryState, entry.Name, mothershippb.Entry_FAILED, nil).Get(ctx, nil)
}()
// Set the entry name to the RPM NVR
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 45 * time.Second,
})
err = workflow.ExecuteActivity(ctx, w.SetEntryIDFromRPM, entry.Name, args.Request.RpmUri, args.Request.Checksum).Get(ctx, &entry)
if err != nil {
return nil, err
}
// Process the RPM.
return processRPMPostHold(ctx, &entry, args)
}
// RetractEntryWorkflow retracts an entry.
// Should be used when an entry debranding is not considered fully complete. (Contains upstream trademarks for example)
// This will forcefully remove the commit from the git repository and set the entry state to RETRACTED.
// The same source (for the specific entry) can be re-imported by the client, either by calling DuplicateEntry or
// calling SubmitEntry with the same SRPM URI.
func RetractEntryWorkflow(ctx workflow.Context, name string) (*mshipadminpb.RetractEntryResponse, error) {
// Set entry state to retracting
var entry mothershippb.Entry
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
})
err := workflow.ExecuteActivity(ctx, w.SetEntryState, name, mothershippb.Entry_RETRACTING, nil).Get(ctx, &entry)
if err != nil {
return nil, err
}
// Deferring this activity will set the entry state to ARCHIVED if the workflow
// is not completed.
defer func() {
if entry.State == mothershippb.Entry_RETRACTED {
return
}
// This is because the entry is still archived, but the commit was not
// retracted.
ctx, cancel := workflow.NewDisconnectedContext(ctx)
defer cancel()
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
})
_ = workflow.ExecuteActivity(ctx, w.SetEntryState, name, mothershippb.Entry_ARCHIVED, nil).Get(ctx, nil)
}()
// Retract commit
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
})
var res mshipadminpb.RetractEntryResponse
err = workflow.ExecuteActivity(ctx, w.RetractEntry, name).Get(ctx, &res)
if err != nil {
return nil, err
}
// Set the entry state to retracted
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 25 * time.Second,
})
err = workflow.ExecuteActivity(ctx, w.SetEntryState, name, mothershippb.Entry_RETRACTED, nil).Get(ctx, &entry)
if err != nil {
return nil, err
}
return &res, nil
}

Some files were not shown because too many files have changed in this diff Show More