From 5cedc5aa15e7bd37e7991218515b1fa6a736b529 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Mon, 19 Oct 2026 08:10:57 +0200 Subject: [PATCH 1/3] fix: prevent removal of services on blocklist relates to STACKITSDK-553 --- scripts/build/copy.go | 37 ++++++++++++++++++++++++ scripts/build/copy_test.go | 37 ++++++++++++++++++++++++ scripts/build/go.sum | 2 ++ scripts/build/main.go | 2 ++ scripts/generate-sdk/languages/go.sh | 26 +++++++++++++++++ scripts/generate-sdk/languages/java.sh | 27 +++++++++++++++++ scripts/generate-sdk/languages/python.sh | 20 +++++++++++++ 7 files changed, 151 insertions(+) create mode 100644 scripts/build/copy.go create mode 100644 scripts/build/copy_test.go create mode 100644 scripts/build/go.sum diff --git a/scripts/build/copy.go b/scripts/build/copy.go new file mode 100644 index 00000000..98b9ae14 --- /dev/null +++ b/scripts/build/copy.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "flag" + "fmt" + "strings" +) + +func runCopy(_ context.Context, args []string, env Environment) error { + copyFlags := flag.NewFlagSet("copy", flag.ContinueOnError) + copyFlags.SetOutput(env.Err) + + var planPath string + copyFlags.StringVar(&planPath, "plan", "", "path to the generation plan") + if err := copyFlags.Parse(args); err != nil { + return fmt.Errorf("parsing copy flags: %w", err) + } + if len(copyFlags.Args()) != 0 { + return fmt.Errorf("unexpected arguments: %s", strings.Join(copyFlags.Args(), " ")) + } + if planPath == "" { + return fmt.Errorf("--plan is required") + } + + plan, err := readPlan(env.FS, planPath) + if err != nil { + return fmt.Errorf("read generation plan: %w", err) + } + + for _, service := range plan.Services { + if service.Action == ActionBlock { + fmt.Fprintln(env.Out, service.Service) + } + } + return nil +} diff --git a/scripts/build/copy_test.go b/scripts/build/copy_test.go new file mode 100644 index 00000000..db1491f7 --- /dev/null +++ b/scripts/build/copy_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "bytes" + "context" + "testing" + "testing/fstest" +) + +func TestRunCopyPrintsServicesMarkedOnBlocklist(t *testing.T) { + filesystem := fstest.MapFS{ + "generation-plan.json": { + Data: []byte(`{ + "services": [ + {"service": "remain", "oas_service": "remain_oas", "action": "block"}, + {"service": "foo", "oas_service": "foo-bar", "action": "generate"}, + {"service": "obsolete", "action": "delete"}, + {"service": "bar", "oas_service": "bar_oas", "action": "block"}, + {"service": "service2", "action": "delete"} + ] +}`), + }, + } + var stdout, stderr bytes.Buffer + + err := runCopy(context.Background(), []string{"--plan", "generation-plan.json"}, Environment{ + Out: &stdout, + Err: &stderr, + FS: readOnlyFileSystem{FileSystem: filesystem}, + }) + if err != nil { + t.Fatalf("runCopy failed: %v", err) + } + if got, want := stdout.String(), "remain\nbar\n"; got != want { + t.Errorf("stdout = %q, want %q", got, want) + } +} diff --git a/scripts/build/go.sum b/scripts/build/go.sum new file mode 100644 index 00000000..40e761ae --- /dev/null +++ b/scripts/build/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/scripts/build/main.go b/scripts/build/main.go index 537584a4..57373a27 100644 --- a/scripts/build/main.go +++ b/scripts/build/main.go @@ -111,6 +111,8 @@ func run(ctx context.Context, env Environment) bool { err = runGenerate(ctx, args[1:], env) case "delete": err = runDelete(ctx, args[1:], env) + case "copy": + err = runCopy(ctx, args[1:], env) default: fmt.Fprintf(env.Err, "unknown command %q\n", args[0]) return false diff --git a/scripts/generate-sdk/languages/go.sh b/scripts/generate-sdk/languages/go.sh index b3db5692..0db19126 100644 --- a/scripts/generate-sdk/languages/go.sh +++ b/scripts/generate-sdk/languages/go.sh @@ -121,6 +121,24 @@ generate_go_sdk() { warning="" + # Copy services from the blocklist to prevent missing dependencies in the examples or removal of the service + mkdir -p "${SERVICES_FOLDER}" + while IFS= read -r service; do + echo "Skipping blocklisted service ${service}" + warning+="Skipping blocklisted service ${service}\n" + # If the service exists in the repo, move them inside the service folder + if [ -d "${sdk_services_backup_dir}/${service}" ]; then + echo "Copy existing ${service} package" + cp -r "${sdk_services_backup_dir}/${service}" "${SERVICES_FOLDER}/${service}" + + if [ -f "${SERVICES_FOLDER}/${service}/go.mod" ]; then + (cd "${SERVICES_FOLDER}/${service}" && go work use .) + fi + fi + done < <( + "${ROOT_DIR}/scripts/bin/build" copy --plan "generation-plan.json" + ) + while IFS= read -r -d '' oas_service && IFS= read -r service; do generate_go_service "${ROOT_DIR}/oas/services/${oas_service}" "${service}" done < <( @@ -164,6 +182,14 @@ generate_go_service() { if grep -E "^${service}-${version}$" "${ROOT_DIR}/languages/golang/blocklist.txt"; then echo "Skipping blocklisted API version ${version} of service ${service}" warning+="Skipping blocklisted API version ${version} of service ${service}\n" + + # If the service version exists in the repo, move them inside the service folder + if [ -d "${sdk_services_backup_dir}/${service}/${version}api" ]; then + echo "Copy existing ${service}/${version}api package" + mkdir -p "${SERVICES_FOLDER}/${service}" + cp -r "${sdk_services_backup_dir}/${service}/${version}api" "${SERVICES_FOLDER}/${service}/${version}api" + fi + continue fi diff --git a/scripts/generate-sdk/languages/java.sh b/scripts/generate-sdk/languages/java.sh index 95b82375..5b420237 100644 --- a/scripts/generate-sdk/languages/java.sh +++ b/scripts/generate-sdk/languages/java.sh @@ -96,6 +96,20 @@ generate_java_sdk() { warning="" + # Copy services from the blocklist to prevent missing dependencies in the examples or removal of the service + mkdir -p "${SERVICES_FOLDER}" + while IFS= read -r service; do + echo "Skipping blocklisted service ${service}" + warning+="Skipping blocklisted service ${service}\n" + # If the service exists in the repo, move them inside the service folder + if [ -d "${sdk_services_backup_dir}/${service}" ]; then + echo "Copy existing ${service} package" + cp -r "${sdk_services_backup_dir}/${service}" "${SERVICES_FOLDER}/${service}" + fi + done < <( + "${ROOT_DIR}/scripts/bin/build" copy --plan "generation-plan.json" + ) + while IFS= read -r -d '' oas_service && IFS= read -r service; do generate_java_service \ "${ROOT_DIR}/oas/services/${oas_service}" \ @@ -139,6 +153,19 @@ generate_java_service(){ if grep -E "^${service}-${version}$" "${ROOT_DIR}/languages/java/blocklist.txt"; then echo "Skipping blocklisted API version ${version} of service ${service}" warning+="Skipping blocklisted API version ${version} of service ${service}\n" + + # If the service version exists in the repo, move them inside the service folder + if [ -d "${sdk_services_backup_dir}/${service}/src/main/java/cloud/stackit/sdk/${service}/${version}api" ]; then + echo "Copy existing ${service}/src/main/java/cloud/stackit/sdk/${service}/${version}api package" + mkdir -p "${SERVICES_FOLDER}/${service}/src/main/java/cloud/stackit/sdk/${service}" + cp -r "${sdk_services_backup_dir}/${service}/src/main/java/cloud/stackit/sdk/${service}/${version}api" "${SERVICES_FOLDER}/${service}/src/main/java/cloud/stackit/sdk/${service}/${version}api" + fi + + if [ -d "${sdk_services_backup_dir}/${service}/src/test/java/cloud/stackit/sdk/${service}/${version}api" ]; then + mkdir -p "${SERVICES_FOLDER}/${service}/src/test/java/cloud/stackit/sdk/${service}" + cp -r "${sdk_services_backup_dir}/${service}/src/test/java/cloud/stackit/sdk/${service}/${version}api" "${SERVICES_FOLDER}/${service}/src/test/java/cloud/stackit/sdk/${service}/${version}api" + fi + continue fi diff --git a/scripts/generate-sdk/languages/python.sh b/scripts/generate-sdk/languages/python.sh index a4ef4ee3..b131a435 100644 --- a/scripts/generate-sdk/languages/python.sh +++ b/scripts/generate-sdk/languages/python.sh @@ -92,6 +92,22 @@ generate_python_sdk() { extra_props_internal="" fi + warning="" + + # Copy services from the blocklist to prevent missing dependencies in the examples or removal of the service + mkdir -p "${SERVICES_FOLDER}" + while IFS= read -r service; do + echo "Skipping blocklisted service ${service}" + warning+="Skipping blocklisted service ${service}\n" + # If the service exists in the repo, move them inside the service folder + if [ -d "${sdk_services_backup_dir}/${service}" ]; then + echo "Copy existing ${service} package" + cp -r "${sdk_services_backup_dir}/${service}" "${SERVICES_FOLDER}/${service}" + fi + done < <( + "${ROOT_DIR}/scripts/bin/build" copy --plan "generation-plan.json" + ) + # TODO: add to generator below when adding multi-API-version support: # --inline-schema-options "SKIP_SCHEMA_REUSE=true" @@ -106,6 +122,10 @@ generate_python_sdk() { done < <( "${ROOT_DIR}/scripts/bin/build" generate --plan "generation-plan.json" ) + + if [[ -n "$warning" ]]; then + echo -e "\nSome of the services were skipped during creation!\n$warning" + fi } generate_python_service() { From abf6ea11570701bfba70734093597ab14a0cf823 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 20 Oct 2026 15:43:52 +0200 Subject: [PATCH 2/3] review feedback --- scripts/generate-sdk/languages/java.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/generate-sdk/languages/java.sh b/scripts/generate-sdk/languages/java.sh index 5b420237..f9cc528d 100644 --- a/scripts/generate-sdk/languages/java.sh +++ b/scripts/generate-sdk/languages/java.sh @@ -161,11 +161,22 @@ generate_java_service(){ cp -r "${sdk_services_backup_dir}/${service}/src/main/java/cloud/stackit/sdk/${service}/${version}api" "${SERVICES_FOLDER}/${service}/src/main/java/cloud/stackit/sdk/${service}/${version}api" fi + # Copy test files for the blocklisted service version if [ -d "${sdk_services_backup_dir}/${service}/src/test/java/cloud/stackit/sdk/${service}/${version}api" ]; then mkdir -p "${SERVICES_FOLDER}/${service}/src/test/java/cloud/stackit/sdk/${service}" cp -r "${sdk_services_backup_dir}/${service}/src/test/java/cloud/stackit/sdk/${service}/${version}api" "${SERVICES_FOLDER}/${service}/src/test/java/cloud/stackit/sdk/${service}/${version}api" fi + # Copy the existing build.gradle file if exists and not already in the services folder + if [[ -f "${sdk_services_backup_dir}/${service}/build.gradle" && ! -f "${SERVICES_FOLDER}/${service}/build.gradle" ]]; then + cp "${sdk_services_backup_dir}/${service}/build.gradle" "${SERVICES_FOLDER}/${service}/build.gradle" + fi + + # Copy the existing README.md file if exists and not already in the services folder + if [[ -f "${sdk_services_backup_dir}/${service}/README.md" && ! -f "${SERVICES_FOLDER}/${service}/README.md" ]]; then + cp "${sdk_services_backup_dir}/${service}/README.md" "${SERVICES_FOLDER}/${service}/README.md" + fi + continue fi From 9beb4d85c56c005c9aa4ca7d03ad4f09e9383e9a Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Fri, 23 Oct 2026 14:09:31 +0200 Subject: [PATCH 3/3] review feedback --- scripts/build/copy.go | 30 ++++- scripts/build/copy_test.go | 261 ++++++++++++++++++++++++++++++++++--- 2 files changed, 274 insertions(+), 17 deletions(-) diff --git a/scripts/build/copy.go b/scripts/build/copy.go index 98b9ae14..c04024e1 100644 --- a/scripts/build/copy.go +++ b/scripts/build/copy.go @@ -7,6 +7,9 @@ import ( "strings" ) +// runCopy implements the `copy --plan ` command. +// It prints all services which have the action "block" in the generation-plan. These services should be copied +// during the generation, to prevent that they will be removed. func runCopy(_ context.Context, args []string, env Environment) error { copyFlags := flag.NewFlagSet("copy", flag.ContinueOnError) copyFlags.SetOutput(env.Err) @@ -28,10 +31,35 @@ func runCopy(_ context.Context, args []string, env Environment) error { return fmt.Errorf("read generation plan: %w", err) } + blockedServices := findBlockedServices(plan) + + return outputResult(env, blockedServices) +} + +// findBlockedServices finds all services in `Plan.Service` which are blocked and returns the service name in a string slice. +func findBlockedServices(plan Plan) []string { + var blockedServices []string for _, service := range plan.Services { if service.Action == ActionBlock { - fmt.Fprintln(env.Out, service.Service) + blockedServices = append(blockedServices, service.Service) } } + + return blockedServices +} + +// outputResult takes a list of blockedServices and writes service in a separate line into `env.Out`. +func outputResult(env Environment, blockedServices []string) error { + if len(blockedServices) == 0 { + return nil + } + + result := fmt.Sprintf("%s\n", strings.Join(blockedServices, "\n")) + + _, err := fmt.Fprint(env.Out, result) + if err != nil { + return fmt.Errorf("writing result: %w", err) + } + return nil } diff --git a/scripts/build/copy_test.go b/scripts/build/copy_test.go index db1491f7..abf95626 100644 --- a/scripts/build/copy_test.go +++ b/scripts/build/copy_test.go @@ -3,35 +3,264 @@ package main import ( "bytes" "context" + "reflect" "testing" "testing/fstest" ) -func TestRunCopyPrintsServicesMarkedOnBlocklist(t *testing.T) { - filesystem := fstest.MapFS{ - "generation-plan.json": { - Data: []byte(`{ +func TestRunCopy(t *testing.T) { + fixtureFlags := []string{"--plan", "generation-plan.json"} + fixtureGenerationPlan := []byte(`{ "services": [ - {"service": "remain", "oas_service": "remain_oas", "action": "block"}, + {"service": "remain", "oas_service": "remain-oas", "action": "block"}, {"service": "foo", "oas_service": "foo-bar", "action": "generate"}, {"service": "obsolete", "action": "delete"}, - {"service": "bar", "oas_service": "bar_oas", "action": "block"}, + {"service": "bar", "oas_service": "bar-oas", "action": "block"}, + {"service": "service2", "action": "delete"} + ] +}`) + + type args struct { + flags []string + generationPlan []byte + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "success", + args: args{ + flags: fixtureFlags, + generationPlan: fixtureGenerationPlan, + }, + want: "remain\nbar\n", + wantErr: false, + }, + { + name: "no blocked services", + args: args{ + flags: fixtureFlags, + generationPlan: []byte(`{ + "services": [ + {"service": "remain", "oas_service": "remain-oas", "action": "generate"}, + {"service": "foo", "oas_service": "foo-bar", "action": "generate"}, + {"service": "obsolete", "action": "delete"}, + {"service": "bar", "oas_service": "bar-oas", "action": "generate"}, {"service": "service2", "action": "delete"} ] }`), + }, + want: "", + wantErr: false, + }, + { + name: "missing flags", + args: args{ + flags: []string{}, + generationPlan: fixtureGenerationPlan, + }, + want: "", + wantErr: true, + }, + { + name: "flag to invalid generationPlan", + args: args{ + flags: []string{"--plan", "not-existing-file.json"}, + generationPlan: fixtureGenerationPlan, + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filesystem := fstest.MapFS{ + "generation-plan.json": { + Data: tt.args.generationPlan, + }, + } + + var stdout bytes.Buffer + env := Environment{ + Out: &stdout, + FS: readOnlyFileSystem{FileSystem: filesystem}, + } + + err := runCopy(context.Background(), tt.args.flags, env) + if (err != nil) != tt.wantErr { + t.Errorf("runCopy() error = %v, wantErr %v", err, tt.wantErr) + } + if got := stdout.String(); got != tt.want { + t.Errorf("runCopy() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFindBlockedServices(t *testing.T) { + type args struct { + plan Plan + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "finds blocked services", + args: args{ + plan: Plan{ + Services: []PlannedService{ + { + Service: "remain", + OASService: "remain-oas", + Action: ActionBlock, + }, + { + Service: "obsolete", + Action: ActionDelete, + }, + { + Service: "foo", + OASService: "foo-bar", + Action: ActionGenerate, + }, + { + Service: "bar", + OASService: "bar-oas", + Action: ActionBlock, + }, + }, + }, + }, + want: []string{"remain", "bar"}, }, + { + name: "no blocked services", + args: args{ + plan: Plan{ + Services: []PlannedService{ + { + Service: "remain", + OASService: "remain-oas", + Action: ActionGenerate, + }, + { + Service: "obsolete", + OASService: "obsolete-oas", + Action: ActionDelete, + }, + { + Service: "foo", + OASService: "foo-bar", + Action: ActionGenerate, + }, + { + Service: "bar", + OASService: "bar-oas", + Action: ActionDelete, + }, + }, + }, + }, + want: nil, + }, + { + name: "all services are blocked", + args: args{ + plan: Plan{ + Services: []PlannedService{ + { + Service: "remain", + OASService: "remain-oas", + Action: ActionBlock, + }, + { + Service: "obsolete", + OASService: "remain-oas", + Action: ActionBlock, + }, + { + Service: "foo", + OASService: "foo-bar", + Action: ActionBlock, + }, + { + Service: "bar", + OASService: "bar-oas", + Action: ActionBlock, + }, + }, + }, + }, + want: []string{"remain", "obsolete", "foo", "bar"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := findBlockedServices(tt.args.plan); !reflect.DeepEqual(got, tt.want) { + t.Errorf("findBlockedServices() = %v, want %v", got, tt.want) + } + }) } - var stdout, stderr bytes.Buffer +} - err := runCopy(context.Background(), []string{"--plan", "generation-plan.json"}, Environment{ - Out: &stdout, - Err: &stderr, - FS: readOnlyFileSystem{FileSystem: filesystem}, - }) - if err != nil { - t.Fatalf("runCopy failed: %v", err) +func TestOutputResult(t *testing.T) { + type args struct { + blockedServices []string } - if got, want := stdout.String(), "remain\nbar\n"; got != want { - t.Errorf("stdout = %q, want %q", got, want) + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "outputs blocked services", + args: args{ + blockedServices: []string{ + "remain", + "obsolete", + }, + }, + want: "remain\nobsolete\n", + wantErr: false, + }, + { + name: "empty blocked services", + args: args{ + blockedServices: []string{}, + }, + want: "", + wantErr: false, + }, + { + name: "nil blocked services", + args: args{ + blockedServices: nil, + }, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var stdout bytes.Buffer + env := Environment{ + Out: &stdout, + } + + if err := outputResult(env, tt.args.blockedServices); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + + got := stdout.String() + if got != tt.want { + t.Errorf("stdout = %q, want %q", got, tt.want) + } + }) } }