mirror of
https://github.com/peridotbuild/peridot.git
synced 2024-06-04 01:00:17 +00:00
Remove mothership from this repo in preparation of OpenELA move
This commit is contained in:
parent
f759b86f85
commit
943a0d60bd
27
tools/mothership/BUILD
vendored
27
tools/mothership/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
74
tools/mothership/admin/rpc/BUILD
vendored
74
tools/mothership/admin/rpc/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
38
tools/mothership/admin/ui/BUILD
vendored
38
tools/mothership/admin/ui/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
|
@ -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
|
||||
}
|
34
tools/mothership/cmd/mship_admin_server/BUILD
vendored
34
tools/mothership/cmd/mship_admin_server/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
33
tools/mothership/cmd/mship_admin_ui/BUILD
vendored
33
tools/mothership/cmd/mship_admin_ui/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
38
tools/mothership/cmd/mship_dev/BUILD
vendored
38
tools/mothership/cmd/mship_dev/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
46
tools/mothership/cmd/mship_server/BUILD
vendored
46
tools/mothership/cmd/mship_server/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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",
|
||||
),
|
||||
],
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
33
tools/mothership/cmd/mship_ui/BUILD
vendored
33
tools/mothership/cmd/mship_ui/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
40
tools/mothership/cmd/mship_worker_server/BUILD
vendored
40
tools/mothership/cmd/mship_worker_server/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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-----
|
32
tools/mothership/db/BUILD
vendored
32
tools/mothership/db/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
);
|
23
tools/mothership/migrations/BUILD
vendored
23
tools/mothership/migrations/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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
|
13
tools/mothership/proto/admin/BUILD
vendored
13
tools/mothership/proto/admin/BUILD
vendored
|
@ -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.
|
64
tools/mothership/proto/admin/v1/BUILD
vendored
64
tools/mothership/proto/admin/v1/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
}
|
66
tools/mothership/proto/v1/BUILD
vendored
66
tools/mothership/proto/v1/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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.
|
50
tools/mothership/rpc/BUILD
vendored
50
tools/mothership/rpc/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
41
tools/mothership/ui/BUILD
vendored
41
tools/mothership/ui/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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 |
|
@ -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
|
||||
}
|
13
tools/mothership/worker_client/BUILD
vendored
13
tools/mothership/worker_client/BUILD
vendored
|
@ -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.
|
22
tools/mothership/worker_client/dnf/BUILD
vendored
22
tools/mothership/worker_client/dnf/BUILD
vendored
|
@ -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"],
|
||||
)
|
|
@ -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)
|
||||
}
|
99
tools/mothership/worker_server/BUILD
vendored
99
tools/mothership/worker_server/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
67
tools/mothership/worker_server/srpm_import/BUILD
vendored
67
tools/mothership/worker_server/srpm_import/BUILD
vendored
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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-----
|
|
@ -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-----
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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-----
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue
Block a user