Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Data Client CI

on:
pull_request:
push:
workflow_dispatch:

concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true

jobs:
lint-and-test:
name: Lint and Unit Tests
runs-on: ubuntu-latest

steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Download dependencies
run: GOTOOLCHAIN=auto go mod download

- name: Run go vet
run: GOTOOLCHAIN=auto go vet ./...

- name: Run unit tests
run: GOTOOLCHAIN=auto go test -v ./...
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
# Build artifacts
/build/
/bin/
checksums.txt
checksums.txt
# Local caches and binaries
/.gocache/
/.tmp/
/data-client
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "ga4gh/data-repository-service-schemas"]
path = ga4gh/data-repository-service-schemas
url = https://github.com/kellrott/data-repository-service-schemas.git
branch = feature/get-by-checksum
25 changes: 24 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ BIN_DIR := ./bin
COVERAGE_THRESHOLD := 30
PACKAGE_COVERAGE_THRESHOLD := 20

# OpenAPI generation now lives in syfon.
SYFON_DIR ?= ../syfon

# --- Targets ---

.PHONY: all build test test-coverage coverage-html coverage-check generate tidy clean help
Expand Down Expand Up @@ -55,6 +58,26 @@ generate:
@echo "--> Running code generation (go generate)..."
@go generate ./...

## gen: Generates Go models from OpenAPI specs
gen:
@set -euo pipefail; \
if [[ ! -d "$(SYFON_DIR)" ]]; then \
echo "ERROR: syfon repo not found at $(SYFON_DIR)"; \
exit 1; \
fi; \
echo "--> OpenAPI generation is centralized in syfon"; \
$(MAKE) -C "$(SYFON_DIR)" gen

.PHONY: gen-internal
gen-internal:
@set -euo pipefail; \
if [[ ! -d "$(SYFON_DIR)" ]]; then \
echo "ERROR: syfon repo not found at $(SYFON_DIR)"; \
exit 1; \
fi; \
echo "--> Internal model generation is centralized in syfon"; \
$(MAKE) -C "$(SYFON_DIR)" gen-internal

## tidy: Cleans up module dependencies and formats go files
tidy:
@echo "--> Tidying go.mod and formatting files..."
Expand All @@ -66,4 +89,4 @@ clean:
@echo "--> Cleaning up..."
@rm -f $(BIN_DIR)/$(TARGET_NAME)
@rm -f coverage.out coverage.html

@rm -rf .tmp
6 changes: 3 additions & 3 deletions cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ func init() {
log.Fatalf("Fatal NewGen3Interface error: %s\n", err)
}

resourceAccess, err := g3i.Fence().CheckPrivileges(context.Background())
resourceAccess, err := g3i.FenceClient().CheckPrivileges(context.Background())
if err != nil {
g3i.Logger().Fatalf("Fatal authentication error: %s\n", err)
} else {
if len(resourceAccess) == 0 {
g3i.Logger().Printf("\nYou don't currently have access to any resources at %s\n", g3i.GetCredential().APIEndpoint)
g3i.Logger().Printf("\nYou don't currently have access to any resources at %s\n", g3i.Credentials().Current().APIEndpoint)
} else {
g3i.Logger().Printf("\nYou have access to the following resource(s) at %s:\n", g3i.GetCredential().APIEndpoint)
g3i.Logger().Printf("\nYou have access to the following resource(s) at %s:\n", g3i.Credentials().Current().APIEndpoint)

// Sort by resource name
resources := make([]string, 0, len(resourceAccess))
Expand Down
123 changes: 61 additions & 62 deletions cmd/collaborator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,13 @@ import (
"gopkg.in/yaml.v3"
)

var collaboratorCmd = &cobra.Command{
Use: "collaborator",
var collaboratorsCmd = &cobra.Command{
Use: "collaborators",
Short: "Manage collaborators and access requests",
}

var emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)

func validateProjectAndUser(projectID, username string) error {
if !emailRegex.MatchString(strings.ToLower(username)) {
return fmt.Errorf("invalid username '%s': must be a valid email address", username)
}

parts := strings.Split(projectID, "-")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid project_id '%s': must be in the form 'program-project'", projectID)
}

return nil
}

func printRequest(r requestor.Request) {
b, err := yaml.Marshal(r)
if err != nil {
Expand All @@ -43,35 +30,37 @@ func printRequest(r requestor.Request) {
fmt.Println(string(b))
}

func getRequestorClient() (requestor.RequestorInterface, func()) {
if profile == "" {
fmt.Println("Error: profile is required. Please specify a profile using the --profile flag.")
func getRequestorClient(localProfile string) (requestor.RequestorInterface, func()) {
if localProfile == "" {
fmt.Println("Error: profile is required.")
os.Exit(1)
}

// Initialize logger
logger, logCloser := logs.New(profile)
logger, logCloser := logs.New(localProfile)

// Initialize Gen3Interface handles selective initialization
g3i, err := g3client.NewGen3Interface(profile, logger, g3client.WithClients(g3client.RequestorClient))
// Initialize base Gen3 interface and build requestor client from it.
g3i, err := g3client.NewGen3Interface(localProfile, logger)
if err != nil {
fmt.Printf("Error accessing Gen3: %v\n", err)
logCloser()
os.Exit(1)
}

return g3i.Requestor(), logCloser
return requestor.NewRequestorClient(g3i, g3i.Credentials().Current()), logCloser
}

var collaboratorListCmd = &cobra.Command{
Use: "ls",
Use: "ls [profile]",
Short: "List requests",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
p := args[0]
mine, _ := cmd.Flags().GetBool("mine")
active, _ := cmd.Flags().GetBool("active")
username, _ := cmd.Flags().GetString("username")

client, closer := getRequestorClient()
client, closer := getRequestorClient(p)
defer closer()

requests, err := client.ListRequests(cmd.Context(), mine, active, username)
Expand All @@ -87,10 +76,12 @@ var collaboratorListCmd = &cobra.Command{
}

var collaboratorPendingCmd = &cobra.Command{
Use: "pending",
Use: "pending [profile]",
Short: "List pending requests",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
client, closer := getRequestorClient()
p := args[0]
client, closer := getRequestorClient(p)
defer closer()

// Fetch all requests
Expand All @@ -110,22 +101,26 @@ var collaboratorPendingCmd = &cobra.Command{
}

var collaboratorAddUserCmd = &cobra.Command{
Use: "add [project_id] [username]",
Use: "add [profile] [email] [program] [project]",
Short: "Add a user to a project",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(2)(cmd, args); err != nil {
return err
}
return validateProjectAndUser(args[0], args[1])
},
Args: cobra.ExactArgs(4),
Run: func(cmd *cobra.Command, args []string) {
projectID := args[0]
p := args[0]
username := args[1]
program := args[2]
project := args[3]
projectID := fmt.Sprintf("%s-%s", program, project)

if !emailRegex.MatchString(strings.ToLower(username)) {
fmt.Printf("Error: invalid email address '%s'\n", username)
os.Exit(1)
}

write, _ := cmd.Flags().GetBool("write")
guppy, _ := cmd.Flags().GetBool("guppy")
approve, _ := cmd.Flags().GetBool("approve")

client, closer := getRequestorClient()
client, closer := getRequestorClient(p)
defer closer()

reqs, err := client.AddUser(cmd.Context(), projectID, username, write, guppy)
Expand Down Expand Up @@ -156,20 +151,24 @@ var collaboratorAddUserCmd = &cobra.Command{
}

var collaboratorRemoveUserCmd = &cobra.Command{
Use: "rm [project_id] [username]",
Use: "rm [profile] [email] [program] [project]",
Short: "Remove a user from a project",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(2)(cmd, args); err != nil {
return err
}
return validateProjectAndUser(args[0], args[1])
},
Args: cobra.ExactArgs(4),
Run: func(cmd *cobra.Command, args []string) {
projectID := args[0]
p := args[0]
username := args[1]
program := args[2]
project := args[3]
projectID := fmt.Sprintf("%s-%s", program, project)

if !emailRegex.MatchString(strings.ToLower(username)) {
fmt.Printf("Error: invalid email address '%s'\n", username)
os.Exit(1)
}

approve, _ := cmd.Flags().GetBool("approve")

client, closer := getRequestorClient()
client, closer := getRequestorClient(p)
defer closer()

reqs, err := client.RemoveUser(cmd.Context(), projectID, username)
Expand Down Expand Up @@ -199,13 +198,14 @@ var collaboratorRemoveUserCmd = &cobra.Command{
}

var collaboratorApproveCmd = &cobra.Command{
Use: "approve [request_id]",
Use: "approve [profile] [request_id]",
Short: "Approve a request (sign it)",
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
requestID := args[0]
p := args[0]
requestID := args[1]

client, closer := getRequestorClient()
client, closer := getRequestorClient(p)
defer closer()

req, err := client.UpdateRequest(cmd.Context(), requestID, "SIGNED")
Expand All @@ -220,15 +220,16 @@ var collaboratorApproveCmd = &cobra.Command{
}

var collaboratorUpdateCmd = &cobra.Command{
Use: "update [request_id] [status]",
Use: "update [profile] [request_id] [status]",
Short: "Update a request status",
Hidden: true,
Args: cobra.ExactArgs(2),
Args: cobra.ExactArgs(3),
Run: func(cmd *cobra.Command, args []string) {
requestID := args[0]
status := args[1]
p := args[0]
requestID := args[1]
status := args[2]

client, closer := getRequestorClient()
client, closer := getRequestorClient(p)
defer closer()

req, err := client.UpdateRequest(cmd.Context(), requestID, status)
Expand All @@ -242,13 +243,13 @@ var collaboratorUpdateCmd = &cobra.Command{
}

func init() {
RootCmd.AddCommand(collaboratorCmd)
collaboratorCmd.AddCommand(collaboratorListCmd)
collaboratorCmd.AddCommand(collaboratorPendingCmd)
collaboratorCmd.AddCommand(collaboratorAddUserCmd)
collaboratorCmd.AddCommand(collaboratorRemoveUserCmd)
collaboratorCmd.AddCommand(collaboratorApproveCmd)
collaboratorCmd.AddCommand(collaboratorUpdateCmd)
RootCmd.AddCommand(collaboratorsCmd)
collaboratorsCmd.AddCommand(collaboratorListCmd)
collaboratorsCmd.AddCommand(collaboratorPendingCmd)
collaboratorsCmd.AddCommand(collaboratorAddUserCmd)
collaboratorsCmd.AddCommand(collaboratorRemoveUserCmd)
collaboratorsCmd.AddCommand(collaboratorApproveCmd)
collaboratorsCmd.AddCommand(collaboratorUpdateCmd)

collaboratorListCmd.Flags().Bool("mine", false, "List my requests")
collaboratorListCmd.Flags().Bool("active", false, "List only active requests")
Expand All @@ -259,6 +260,4 @@ func init() {
collaboratorAddUserCmd.Flags().BoolP("approve", "a", false, "Automatically approve the requests")

collaboratorRemoveUserCmd.Flags().BoolP("approve", "a", false, "Automatically approve the revoke requests")

collaboratorCmd.PersistentFlags().StringVar(&profile, "profile", "", "Specify profile to use")
}
2 changes: 1 addition & 1 deletion cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func init() {
}

g3i := g3client.NewGen3InterfaceFromCredential(cred, logger, g3client.WithClients())
err := g3i.ExportCredential(context.Background(), cred)
err := g3i.Credentials().Export(context.Background(), cred)
if err != nil {
logger.Println(err.Error())
}
Expand Down
42 changes: 0 additions & 42 deletions cmd/delete.go

This file was deleted.

Loading
Loading