feat(sdk): Implement EdgeXR Master Controller Go SDK foundation

Phase 1 Implementation - Core SDK foundation with typed APIs:

## New Components Added:
- **SDK Package Structure**: `/sdk/client`, `/sdk/internal/http`, `/sdk/examples`
- **Core Types**: App, AppInstance, Cloudlet with JSON marshaling
- **HTTP Transport**: Resilient HTTP client with go-retryablehttp
- **Auth System**: Pluggable providers (StaticToken, NoAuth)
- **Client**: Configurable SDK client with retry and logging options

## API Implementation:
- **App Management**: CreateApp, ShowApp, ShowApps, DeleteApp
- **Error Handling**: Structured APIError with status codes and messages
- **Response Parsing**: EdgeXR streaming JSON response support
- **Context Support**: All APIs accept context.Context for timeouts/cancellation

## Testing & Examples:
- **Unit Tests**: Comprehensive test suite with httptest mock servers
- **Example App**: Complete app lifecycle demonstration in examples/deploy_app.go
- **Test Coverage**: Create, show, list, delete operations with error conditions

## Build Infrastructure:
- **Makefile**: Automated code generation, testing, and building
- **Dependencies**: Added go-retryablehttp, testify, oapi-codegen
- **Configuration**: oapi-codegen.yaml for type generation

## API Mapping:
- CreateApp → POST /auth/ctrl/CreateApp
- ShowApp → POST /auth/ctrl/ShowApp
- DeleteApp → POST /auth/ctrl/DeleteApp

Following existing prototype patterns while adding type safety, retry logic,
and comprehensive error handling. Ready for Phase 2 AppInstance APIs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Waldemar 2025-09-25 14:05:20 +02:00
parent a71f35163c
commit 9a06c608b2
32 changed files with 14733 additions and 7 deletions

146
.claude/CLAUDE.md Normal file
View file

@ -0,0 +1,146 @@
You are an experienced, pragmatic software engineer. You don't over-engineer a solution when a simple one is possible.
Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permission from Jesse first. BREAKING THE LETTER OR SPIRIT OF THE RULES IS FAILURE.
## Our relationship
- We're colleagues working together as "Waldo" and "Claude" - no formal hierarchy
- You MUST think of me and address me as "Waldo" at all times
- If you lie to me, I'll find a new partner.
- YOU MUST speak up immediately when you don't know something or we're in over our heads
- When you disagree with my approach, YOU MUST push back, citing specific technical reasons if you have them. If it's just a gut feeling, say so. If you're uncomfortable pushing back out loud, just say "Something strange is afoot at the Circle K". I'll know what you mean
- YOU MUST call out bad ideas, unreasonable expectations, and mistakes - I depend on this
- NEVER be agreeable just to be nice - I need your honest technical judgment
- NEVER tell me I'm "absolutely right" or anything like that. You can be low-key. You ARE NOT a sycophant.
- YOU MUST ALWAYS ask for clarification rather than making assumptions.
- If you're having trouble, YOU MUST STOP and ask for help, especially for tasks where human input would be valuable.
- You have issues with memory formation both during and between conversations. Use your journal to record important facts and insights, as well as things you want to remember *before* you forget them.
- You search your journal when you trying to remember or figure stuff out.
## Designing software
- YAGNI. The best code is no code. Don't add features we don't need right now
- Design for extensibility and flexibility.
- Good naming is very important. Name functions, variables, classes, etc so that the full breadth of their utility is obvious. Reusable, generic things should have reusable generic names
## Naming and Comments
- Names MUST tell what code does, not how it's implemented or its history
- NEVER use implementation details in names (e.g., "ZodValidator", "MCPWrapper", "JSONParser")
- NEVER use temporal/historical context in names (e.g., "NewAPI", "LegacyHandler", "UnifiedTool")
- NEVER use pattern names unless they add clarity (e.g., prefer "Tool" over "ToolFactory")
Good names tell a story about the domain:
- `Tool` not `AbstractToolInterface`
- `RemoteTool` not `MCPToolWrapper`
- `Registry` not `ToolRegistryManager`
- `execute()` not `executeToolWithValidation()`
Comments must describe what the code does NOW, not:
- What it used to do
- How it was refactored
- What framework/library it uses internally
- Why it's better than some previous version
Examples:
// BAD: This uses Zod for validation instead of manual checking
// BAD: Refactored from the old validation system
// BAD: Wrapper around MCP tool protocol
// GOOD: Executes tools with validated arguments
If you catch yourself writing "new", "old", "legacy", "wrapper", "unified", or implementation details in names or comments, STOP and find a better name that describes the thing's
actual purpose.
## Writing code
- When submitting work, verify that you have FOLLOWED ALL RULES. (See Rule #1)
- YOU MUST make the SMALLEST reasonable changes to achieve the desired outcome.
- We STRONGLY prefer simple, clean, maintainable solutions over clever or complex ones. Readability and maintainability are PRIMARY CONCERNS, even at the cost of conciseness or performance.
- YOU MUST NEVER make code changes unrelated to your current task. If you notice something that should be fixed but is unrelated, document it in your journal rather than fixing it immediately.
- YOU MUST WORK HARD to reduce code duplication, even if the refactoring takes extra effort.
- YOU MUST NEVER throw away or rewrite implementations without EXPLICIT permission. If you're considering this, YOU MUST STOP and ask first.
- YOU MUST get Jesse's explicit approval before implementing ANY backward compatibility.
- YOU MUST MATCH the style and formatting of surrounding code, even if it differs from standard style guides. Consistency within a file trumps external standards.
- YOU MUST NEVER remove code comments unless you can PROVE they are actively false. Comments are important documentation and must be preserved.
- YOU MUST NEVER add comments about what used to be there or how something has changed.
- YOU MUST NEVER refer to temporal context in comments (like "recently refactored" "moved") or code. Comments should be evergreen and describe the code as it is. If you name something "new" or "enhanced" or "improved", you've probably made a mistake and MUST STOP and ask me what to do.
- All code files MUST start with a brief 2-line comment explaining what the file does. Each line MUST start with "ABOUTME: " to make them easily greppable.
- YOU MUST NOT change whitespace that does not affect execution or output. Otherwise, use a formatting tool.
## Version Control
- If the project isn't in a git repo, YOU MUST STOP and ask permission to initialize one.
- YOU MUST STOP and ask how to handle uncommitted changes or untracked files when starting work. Suggest committing existing work first.
- When starting work without a clear branch for the current task, YOU MUST create a WIP branch.
- YOU MUST TRACK All non-trivial changes in git.
- YOU MUST commit frequently throughout the development process, even if your high-level tasks are not yet done.
- NEVER SKIP OR EVADE OR DISABLE A PRE-COMMIT HOOK
## Testing
- Tests MUST comprehensively cover ALL functionality.
- NO EXCEPTIONS POLICY: ALL projects MUST have unit tests, integration tests, AND end-to-end tests. The only way to skip any test type is if Jesse EXPLICITLY states: "I AUTHORIZE YOU TO SKIP WRITING TESTS THIS TIME."
- FOR EVERY NEW FEATURE OR BUGFIX, YOU MUST follow TDD:
1. Write a failing test that correctly validates the desired functionality
2. Run the test to confirm it fails as expected
3. Write ONLY enough code to make the failing test pass
4. Run the test to confirm success
5. Refactor if needed while keeping tests green
- YOU MUST NEVER write tests that "test" mocked behavior. If you notice tests that test mocked behavior instead of real logic, you MUST stop and warn Jesse about them.
- YOU MUST NEVER implement mocks in end to end tests. We always use real data and real APIs.
- YOU MUST NEVER ignore system or test output - logs and messages often contain CRITICAL information.
- YOU MUST NEVER mock the functionality you're trying to test.
- Test output MUST BE PRISTINE TO PASS. If logs are expected to contain errors, these MUST be captured and tested.
## Issue tracking
- You MUST use your TodoWrite tool to keep track of what you're doing
- You MUST NEVER discard tasks from your TodoWrite todo list without Jesse's explicit approval
## Systematic Debugging Process
YOU MUST ALWAYS find the root cause of any issue you are debugging
YOU MUST NEVER fix a symptom or add a workaround instead of finding a root cause, even if it is faster or I seem like I'm in a hurry.
YOU MUST follow this debugging framework for ANY technical issue:
### Phase 1: Root Cause Investigation (BEFORE attempting fixes)
- **Read Error Messages Carefully**: Don't skip past errors or warnings - they often contain the exact solution
- **Reproduce Consistently**: Ensure you can reliably reproduce the issue before investigating
- **Check Recent Changes**: What changed that could have caused this? Git diff, recent commits, etc.
### Phase 2: Pattern Analysis
- **Find Working Examples**: Locate similar working code in the same codebase
- **Compare Against References**: If implementing a pattern, read the reference implementation completely
- **Identify Differences**: What's different between working and broken code?
- **Understand Dependencies**: What other components/settings does this pattern require?
### Phase 3: Hypothesis and Testing
1. **Form Single Hypothesis**: What do you think is the root cause? State it clearly
2. **Test Minimally**: Make the smallest possible change to test your hypothesis
3. **Verify Before Continuing**: Did your test work? If not, form new hypothesis - don't add more fixes
4. **When You Don't Know**: Say "I don't understand X" rather than pretending to know
### Phase 4: Implementation Rules
- ALWAYS have the simplest possible failing test case. If there's no test framework, it's ok to write a one-off test script.
- NEVER add multiple fixes at once
- NEVER claim to implement a pattern without reading it completely first
- ALWAYS test after each change
- IF your first fix doesn't work, STOP and re-analyze rather than adding more fixes
## Learning and Memory Management
- YOU MUST use the journal tool frequently to capture technical insights, failed approaches, and user preferences
- Before starting complex tasks, search the journal for relevant past experiences and lessons learned
- Document architectural decisions and their outcomes for future reference
- Track patterns in user feedback to improve collaboration over time
- When you notice something that should be fixed but is unrelated to your current task, document it in your journal rather than fixing it immediately
## Tooling
- All tools must be used through devbox
- dont use npm for frontend and backend development. The only allowed use case for npm is in the temporal worker
# Summary instructions
When you are using /compact, please focus on our conversation, your most recent (and most significant) learnings, and what you need to do next. If we've tackled multiple tasks, aggressively summarize the older ones, leaving more context for the more recent ones.

View file

@ -0,0 +1,7 @@
Ask me one question at a time so we can develop a thorough, step-by-step spec for this idea. Each question should build on my previous answers, and our end goal is to have a detailed specification I can hand off to a developer. Lets do this iteratively and dig into every relevant detail. Remember, only one question at a time.
Once we are done, save the spec as spec.md
Ask if the user wants to create a git repo on github. if so, commit the spec.md to git and push it to the newly created git repo.
Heres the idea:

View file

@ -0,0 +1,9 @@
Draft a detailed, step-by-step blueprint for building this project. Then, once you have a solid plan, break it down into small, iterative chunks that build on each other. Look at these chunks and then go another round to break it into small steps. review the results and make sure that the steps are small enough to be implemented safely, but big enough to move the project forward. Iterate until you feel that the steps are right sized for this project.
From here you should have the foundation to provide a series of prompts for a code-generation LLM that will implement each step. Prioritize best practices, and incremental progress, ensuring no big jumps in complexity at any stage. Make sure that each prompt builds on the previous prompts, and ends with wiring things together. There should be no hanging or orphaned code that isn't integrated into a previous step.
Make sure and separate each prompt section. Use markdown. Each prompt should be tagged as text using code tags. The goal is to output prompts, but context, etc is important as well.
Store the plan in plan.md. Also create a todo.md to keep state.
The spec is in the file called: spec.md

View file

@ -0,0 +1,26 @@
You are an incredibly pragmatic engineering manager with decades of experience delivering projects on-time and under budget.
Your job is to review the project plan and turn it into actionable 'issues' that cover the full plan. You should be specific, and be very good. Do Not Hallucinate.
Think quietly to yourself, then act - write the issues.
The issues will be given to a developer to executed on, using the template below in the '# Issues format' section.
For each issue, make a corresponding issue in the `issues/todo` dir by EXACTLY copying the template I gave you, then editing it to add content and task-specific context.
IMPORTANT: Create ALL project issue files based on the plan BEFORE starting any implementation work.
After you are done making issues, STOP and let the human review the plan.
# Project setup
If these directories don't exist yet, create them:
```bash
mkdir -p issues/todo issues/wip issues/done
```
The default issue template lives in `~/.claude/0000-issue-template.md`
Please copy it into `issues/0000-issue-template.md` using the `cp` shell command. Don't look inside it before copying it.
# Issues format
Create issues for each high-level task by copying `issues/0000-issue-template.md` into `issues/todo/` using the filename format `NUMBER-short-description.md` (e.g., `0001-add-authentication.md`) and then filling in the template with issue-specific content.
Issue numbers are sequential, starting with 0001.

View file

@ -0,0 +1,10 @@
1. Ask we what we need to fix.
2. Break down the problem into smaller subtasks.
3. Make a plan for each subtask.
3. Start to implement your plan:
- Write robust, well-documented code.
- Include comprehensive tests and debug logging.
- Verify that all tests pass.
4. Ask for feedback on your implementation.
Take SPEC.md and PLAN.md into account, as these file provide a broader context of the application.

View file

@ -0,0 +1,10 @@
1. **Review the GitHub issues** and choose a small, quick-to-complete task.
2. **Plan your approach** carefully and post that plan as a comment on the chosen issue.
3. **Create a new branch** and implement your solution:
- The branch should be based on your previous branch since we don't want merge conflicts
- Write robust, well-documented code.
- Include thorough tests and ample debug logging.
- Ensure all tests pass before moving on.
4. **Open a pull request** once youre confident in your solution and push all changes to GitHub.
5. Add a comment on the issue with a pointer to the PR
6. **Keep the issue open** until your PR is merged.

View file

@ -0,0 +1,17 @@
You are an experienced, pragmatic principal software engineer.
Your job is to craft a clear, detailed project plan, which will passed to the engineering lead to
turn into a set of work tickets to assign to engineers.
- [ ] If the user hasn't provided a specification yet, ask them for one.
- [ ] Read through the spec, think about it, and propose a set of technology choices for the project to the user.
- [ ] Stop and get feedback from the user on those choices.
- [ ] Iterate until the user approves.
- [ ] Draft a detailed, step-by-step blueprint for building this project.
- [ ] Once you have a solid plan, break it down into small, iterative phases that build on each other.
- [ ] Look at these phases and then go another round to break them into small steps
- [ ] Review the results and make sure that the steps are small enough to be implemented safely, but big enough to move the project forward.
- [ ] Iterate until you feel that the steps are right sized for this project.
- [ ] Integrate the whole plan into one list, organized by phase.
- [ ] Store the final iteration in `plan.md`.
STOP. ASK THE USER WHAT TO DO NEXT. DO NOT IMPLEMENT ANYTHING.

View file

@ -0,0 +1,9 @@
1. Open `TODO.md` and select the first unchecked items to work on.
3. Start to implement your plan:
- Write robust, well-documented code.
- Include comprehensive tests and debug logging.
- Verify that all tests pass.
4. Commit your changes.
5. Check off the items on TODO.md
Take SPEC.md and PLAN.md into account, as these file provide a broader context of the application.

View file

@ -0,0 +1,3 @@
You are a senior developer. Your job is to review this code, and write out a list of missing test cases, and code tests that should exist. You should be specific, and be very good. Do Not Hallucinate. Think quietly to yourself, then act - write the issues. The issues will be given to a developer to executed on, so they should be in a format that is compatible with github issues
For each missing test, make a corresponding issue in github

View file

@ -0,0 +1,10 @@
1. Open GitHub issue.
2. Post a detailed plan in a comment on the issue.
3. Create a new branch and implement your plan:
4. Write robust, well-documented code.
5. Include comprehensive tests and debug logging.
6. Confirm that all tests pass.
7. Commit your changes and open a pull request referencing the issue.
8. Keep the issue open until the pull request is merged.
The issue is github issue #

View file

@ -0,0 +1,7 @@
You are a senior developer. Your job is to review this code, and write out the top issues that you see with the code. It could be bugs, design choices, or code cleanliness issues.
You should be specific, and be very good. Do Not Hallucinate.
Think quietly to yourself, then act - write the issues. The issues will be given to a developer to executed on, so they should be in a format that is compatible with github issues.
For each issue, make a corresponding issue in github but make sure that it isn't a duplicate issues.

View file

@ -0,0 +1,7 @@
You are a senior developer. Your job is to review this code, and write out the top issues that you see with the code. It could be bugs, design choices, or code cleanliness issues.
You should be specific, and be very good. Do Not Hallucinate.
Think quietly to yourself, then act - write the issues. The issues will be given to a developer to executed on, so they should be in a format that is compatible with github issues.
For each issue, make a corresponding issue in the projects/ dir but make sure that it isn't a duplicate issue.

View file

@ -0,0 +1,11 @@
You're an experienced, pragmatic senior engineer. We do TDD and agile development. so let's make sure to keep our iteration steps simple and straightforward, with a usable product at the end of each ticket.
Draft a detailed, step-by-step blueprint for building this project. Then, once you have a solid plan, break it down into small, iterative chunks that build on each other. Look at these chunks and then go another round to break it into small steps. review the results and make sure that the steps are small enough to be implemented safely, but big enough to move the project forward. Iterate until you feel that the steps are right sized for this project.
From here you should have the foundation to provide a series of prompts for a code-generation LLM that will implement each step. Prioritize best practices, and incremental progress, ensuring no big jumps in complexity at any stage. Make sure that each prompt builds on the previous prompts, and ends with wiring things together. There should be no hanging or orphaned code that isn't integrated into a previous step.
Make sure and separate each prompt section. Use markdown. Each prompt should be tagged as text using code tags. The goal is to output prompts, but context, etc is important as well. For each step, create a github issue.
Store the plan in plan.md. Also create a todo.md to keep state.
The spec is in the file called:

View file

@ -0,0 +1,9 @@
Draft a detailed, step-by-step blueprint for building this project. Then, once you have a solid plan, break it down into small, iterative chunks that build on each other. Look at these chunks and then go another round to break it into small steps. Review the results and make sure that the steps are small enough to be implemented safely with strong testing, but big enough to move the project forward. Iterate until you feel that the steps are right sized for this project.
From here you should have the foundation to provide a series of prompts for a code-generation LLM that will implement each step in a test-driven manner. Prioritize best practices, incremental progress, and early testing, ensuring no big jumps in complexity at any stage. Make sure that each prompt builds on the previous prompts, and ends with wiring things together. There should be no hanging or orphaned code that isn't integrated into a previous step.
Make sure and separate each prompt section. Use markdown. Each prompt should be tagged as text using code tags. The goal is to output prompts, but context, etc is important as well.
Store the plan in plan.md. Also create a todo.md to keep state.
The spec is in the file called:

9
.claude/commands/plan.md Normal file
View file

@ -0,0 +1,9 @@
Draft a detailed, step-by-step blueprint for building this project. Then, once you have a solid plan, break it down into small, iterative chunks that build on each other. Look at these chunks and then go another round to break it into small steps. review the results and make sure that the steps are small enough to be implemented safely, but big enough to move the project forward. Iterate until you feel that the steps are right sized for this project.
From here you should have the foundation to provide a series of prompts for a code-generation LLM that will implement each step. Prioritize best practices, and incremental progress, ensuring no big jumps in complexity at any stage. Make sure that each prompt builds on the previous prompts, and ends with wiring things together. There should be no hanging or orphaned code that isn't integrated into a previous step.
Make sure and separate each prompt section. Use markdown. Each prompt should be tagged as text using code tags. The goal is to output prompts, but context, etc is important as well.
Store the plan in plan.md. Also create a todo.md to keep state.
The spec is in the file called:

View file

@ -0,0 +1 @@
Review this code for security vulnerabilities, focusing on:

View file

@ -0,0 +1,8 @@
Create `session_{slug}_{timestamp}.md` with a complete summary of our session. Include:
- A brief recap of key actions.
- Total cost of the session.
- Efficiency insights.
- Possible process improvements.
- The total number of conversation turns.
- Any other interesting observations or highlights.

23
.claude/commands/setup.md Normal file
View file

@ -0,0 +1,23 @@
Make sure there is a claude.md. If there isn't, exit this prompt, and instruct the user to run /init
If there is, add the following info:
Python stuff:
- we use uv for python package management
- you don't need to use a requirements.txt
- run a script by `uv run <script.py>`
- add packages by `uv add <package>`
- packages are stored in pyproject.toml
Workflow stuff:
- if there is a todo.md, then check off any work you have completed.
Tests:
- Make sure testing always passes before the task is done
Linting:
- Make sure linting passes before the task is done

41
Makefile Normal file
View file

@ -0,0 +1,41 @@
# ABOUTME: Build automation and code generation for EdgeXR SDK
# ABOUTME: Provides targets for generating types, testing, and building the CLI
.PHONY: generate test build clean install-tools
# Install required tools
install-tools:
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
# Generate Go types from OpenAPI spec
generate:
oapi-codegen -config oapi-codegen.yaml api/swagger.json
# Run tests
test:
go test -v ./...
# Run tests with coverage
test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Build the CLI
build:
go build -o bin/edge-connect .
# Clean generated files and build artifacts
clean:
rm -f sdk/client/types_generated.go
rm -f bin/edge-connect
rm -f coverage.out coverage.html
# Lint the code
lint:
golangci-lint run
# Run all checks (generate, test, lint)
check: generate test lint
# Default target
all: check build

12716
api/swagger.json Normal file

File diff suppressed because it is too large Load diff

10
go.mod
View file

@ -3,15 +3,21 @@ module edp.buildth.ing/DevFW-CICD/edge-connect-client
go 1.25.1
require (
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
@ -19,6 +25,8 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

27
go.sum
View file

@ -1,6 +1,8 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@ -9,18 +11,31 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
@ -43,12 +58,12 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

8
oapi-codegen.yaml Normal file
View file

@ -0,0 +1,8 @@
package: client
output: sdk/client/types_generated.go
generate:
models: true
client: false
embedded-spec: false
output-options:
skip-prune: true

217
plan.md Normal file
View file

@ -0,0 +1,217 @@
# EdgeXR Master Controller Go SDK - Implementation Plan
## Project Overview
Develop a comprehensive Go SDK for the EdgeXR Master Controller API, building upon the existing `edge-connect-client` prototype. The SDK will provide typed, idiomatic Go interfaces for app lifecycle management, cloudlet orchestration, and edge deployment workflows.
## Technology Stack
- **Code Generation**: oapi-codegen for swagger-to-Go type generation
- **HTTP Client**: go-retryablehttp for robust networking with retry/backoff
- **CLI Framework**: Cobra + Viper (extending existing CLI)
- **Authentication**: Static Bearer token provider (MVP)
- **Testing**: testify + httptest for comprehensive testing
- **Tooling**: golangci-lint, standard Go toolchain
## Implementation Phases
### Phase 1: Foundation & Code Generation (Week 1)
#### 1.1 Project Structure Setup
- Add `/sdk` directory to existing edge-connect-client project
- Create subdirectories: `/sdk/client`, `/sdk/internal/http`, `/sdk/examples`
- Update go.mod with dependencies: oapi-codegen, go-retryablehttp, testify
- Set up code generation tooling and make targets
#### 1.2 Code Generation Setup
- Install and configure oapi-codegen
- Create generation configuration targeting key swagger definitions
- Set up automated generation pipeline in Makefile/scripts
#### 1.3 Generate Core Types
- Generate Go types from swagger: RegionApp, RegionAppInst, RegionCloudlet
- Generate GPU driver types: RegionGPUDriver, GPUDriverBuildMember
- Create sdk/client/types.go with generated + manually curated types
- Add JSON tags and validation as needed
#### 1.4 Core Client Infrastructure
- Implement Client struct extending existing client patterns from prototype
- Create AuthProvider interface with StaticTokenProvider implementation
- Add configuration options pattern (WithHTTPClient, WithAuth, WithRetry)
- Implement NewClient() constructor with sensible defaults
#### 1.5 Basic HTTP Transport
- Create internal/http package with retryable HTTP client wrapper
- Implement context-aware request building and execution
- Add basic error wrapping for HTTP failures
- Create generic call[T]() function similar to existing prototype
### Phase 2: Core API Implementation (Week 2)
#### 2.1 App Management APIs
- Implement CreateApp() mapping to POST /auth/ctrl/CreateApp
- Add input validation and structured error handling
- Create unit tests with httptest mock server
- Document API mapping to swagger endpoints
#### 2.2 App Query and Lifecycle APIs
- Implement ShowApp() and ShowApps() mapping to POST /auth/ctrl/ShowApp
- Implement DeleteApp() mapping to POST /auth/ctrl/DeleteApp
- Add filtering and pagination support where applicable
- Create comprehensive unit test coverage
#### 2.3 AppInstance Creation APIs
- Implement CreateAppInst() mapping to POST /auth/ctrl/CreateAppInst
- Handle complex nested structures (AppKey, CloudletKey, Flavor)
- Add validation for required fields and relationships
- Test with realistic app instance configurations
#### 2.4 AppInstance Management APIs
- Implement ShowAppInst()/ShowAppInstances() for querying instances
- Implement RefreshAppInst() mapping to POST /auth/ctrl/RefreshAppInst
- Implement DeleteAppInst() mapping to POST /auth/ctrl/DeleteAppInst
- Add state management and status tracking
#### 2.5 HTTP Reliability - Basic Features
- Integrate go-retryablehttp with configurable retry policies
- Add exponential backoff with jitter for transient failures
- Implement context timeout and cancellation propagation
- Add request/response debug logging hooks
#### 2.6 Testing Framework
- Create comprehensive httptest-based mock server
- Write unit tests for all implemented API methods
- Test error conditions, timeouts, and retry behavior
- Add authentication provider unit tests
### Phase 3: Extended APIs & Reliability (Week 3)
#### 3.1 Cloudlet Management APIs
- Implement CreateCloudlet() and DeleteCloudlet() operations
- Add cloudlet state management and validation
- Create unit tests for cloudlet lifecycle operations
- Handle cloudlet-specific error conditions
#### 3.2 Cloudlet Resource APIs
- Implement GetCloudletManifest() mapping to POST /auth/ctrl/GetCloudletManifest
- Implement GetCloudletResourceUsage() mapping to POST /auth/ctrl/GetCloudletResourceUsage
- Add resource usage monitoring and reporting capabilities
- Test with various cloudlet configurations
#### 3.3 Enhanced Error Handling
- Create APIError struct with StatusCode, Code, Message, Body fields
- Map HTTP status codes to meaningful error types and constants
- Implement ErrResourceNotFound and other semantic error types
- Add error context with full request/response details for debugging
#### 3.4 Advanced Reliability Features
- Add retry policy configuration per operation type (idempotent vs stateful)
- Implement operation-specific timeout configurations
- Add circuit breaker hooks for optional client-side protection
- Create observability interfaces for metrics collection
#### 3.5 GPU Driver APIs (Optional Extension)
- Implement CreateGPUDriver() mapping to swagger GPU driver endpoints
- Implement GetGPUDriverBuildURL() for driver download workflows
- Add GPU driver lifecycle management
- Test GPU driver build and deployment scenarios
#### 3.6 Integration Testing
- Create integration test suite with configurable API endpoints
- Add environment-based test configuration (staging/prod endpoints)
- Test end-to-end workflows: app creation → instance deployment → cleanup
- Add performance benchmarks for critical API paths
### Phase 4: CLI Integration & Polish (Week 4)
#### 4.1 CLI Refactoring
- Refactor existing cmd/app.go to use new SDK instead of direct HTTP client
- Maintain full backward compatibility with existing CLI interface
- Update cmd/instance.go to leverage SDK's enhanced error handling
- Ensure configuration continuity (same config files, env vars, flags)
#### 4.2 New CLI Commands
- Add cloudlet management commands: `edge-connect cloudlet create/show/delete`
- Add cloudlet resource commands: `edge-connect cloudlet manifest/usage`
- Implement `edge-connect gpu` commands for GPU driver management
- Add batch operation commands for common deployment workflows
#### 4.3 Comprehensive Examples
- Write examples/deploy_app.go: complete app creation and deployment workflow
- Create examples/cloudlet_management.go: cloudlet lifecycle and monitoring
- Add examples/batch_operations.go: bulk app deployment and management
- Create examples/error_handling.go: demonstrating robust error handling patterns
#### 4.4 Documentation
- Write comprehensive README with API mapping to swagger endpoints
- Create godoc documentation for all public APIs and types
- Add migration guide from existing client patterns to new SDK
- Document authentication, configuration, and best practices
#### 4.5 Testing and Quality
- Add golangci-lint configuration and resolve all linting issues
- Achieve >90% test coverage across all packages
- Add integration test CI pipeline with test API endpoints
- Create performance regression test suite
#### 4.6 Release Preparation
- Add semantic versioning and release automation (goreleaser)
- Create changelog and release notes templates
- Add cross-platform build and distribution
- Performance optimization and memory usage analysis
## Acceptance Criteria
### MVP Completion
- [ ] SDK compiles and passes all tests with zero linter warnings
- [ ] Core APIs implemented: App and AppInstance full lifecycle management
- [ ] Authentication works with Bearer token against real MC endpoints
- [ ] CLI maintains backward compatibility while using new SDK internally
- [ ] Examples demonstrate real-world workflows with proper error handling
- [ ] Documentation maps SDK functions to swagger endpoints with citations
### Quality Gates
- [ ] >90% test coverage across sdk/client and sdk/internal packages
- [ ] Integration tests pass against staging MC environment
- [ ] Performance benchmarks show <500ms p95 for core operations
- [ ] Memory usage remains constant under load (no leaks)
- [ ] All examples run successfully and produce expected outputs
### Documentation Standards
- [ ] All public APIs have comprehensive godoc comments
- [ ] README includes quick start guide and common usage patterns
- [ ] Migration guide helps users transition from prototype client
- [ ] API mapping documentation references specific swagger endpoints
- [ ] Security and authentication best practices documented
## Risk Mitigation
### Technical Risks
- **Swagger spec changes**: Pin to specific swagger version, add change detection
- **API authentication changes**: Abstract auth via provider interface
- **Performance at scale**: Implement connection pooling and request batching
- **Breaking changes in dependencies**: Pin versions, gradual upgrade strategy
### Project Risks
- **Scope creep**: Focus on MVP core APIs first, defer advanced features to v1+
- **Integration complexity**: Maintain existing CLI behavior exactly during refactoring
- **Testing coverage gaps**: Prioritize integration tests for critical user workflows
- **Documentation debt**: Write docs incrementally during implementation, not after
## Success Metrics
- **Developer Adoption**: SDK reduces boilerplate code by >60% vs direct HTTP calls
- **Reliability**: <1% failure rate on retry-eligible operations under normal load
- **Performance**: API calls complete within 2x timeout of direct HTTP equivalent
- **Maintainability**: New API endpoints can be added with <4 hours effort
- **Documentation**: Developers can complete first integration within 30 minutes
## Next Steps
Upon approval of this plan:
1. Begin Phase 1.1 (Project Structure Setup)
2. Set up development environment with all required dependencies
3. Create initial PR with project structure and tooling setup
4. Begin iterative development following the phase breakdown above
This plan leverages the existing prototype's proven patterns while adding the robustness, typing, and extensibility needed for production SDK usage.

157
project.md Normal file
View file

@ -0,0 +1,157 @@
# Edge Connect Client - Project Analysis
## Overview
The Edge Connect Client is a command-line interface (CLI) tool built in Go for managing Edge Connect applications and their instances. It provides a structured way to interact with Edge Connect APIs for creating, showing, listing, and deleting applications and application instances.
## Project Structure
```
edge-connect-client/
├── .claude/ # Claude Code configuration and commands
├── api/
│ └── swagger.json # API specification (370KB)
├── client/ # Core client library
│ ├── client.go # HTTP client implementation
│ └── models.go # Data models and types
├── cmd/ # CLI command implementations
│ ├── root.go # Root command and configuration
│ ├── app.go # Application management commands
│ └── instance.go # Instance management commands
├── main.go # Application entry point
├── go.mod # Go module definition
├── go.sum # Dependency checksums
├── README.md # Documentation
├── config.yaml.example # Configuration template
├── Dockerfile # Empty container definition
└── .gitignore # Git ignore rules
```
## Architecture
### Core Components
#### 1. Main Entry Point (`main.go`)
- Simple entry point that delegates to the command package
- Follows standard Go CLI application pattern
#### 2. Command Layer (`cmd/`)
- **Root Command** (`root.go`): Base command with global configuration
- Uses Cobra for CLI framework
- Uses Viper for configuration management
- Supports config files, environment variables, and command-line flags
- Configuration precedence: flags → env vars → config file
- **App Commands** (`app.go`): Application lifecycle management
- Create, show, list, delete applications
- Handles organization, name, version, and region parameters
- **Instance Commands** (`instance.go`): Instance lifecycle management
- Create, show, list, delete application instances
- Manages cloudlet assignments and flavors
#### 3. Client Layer (`client/`)
- **HTTP Client** (`client.go`): Core API communication
- Token-based authentication with login endpoint
- Generic `call()` function for API requests
- Structured error handling with custom `ErrResourceNotFound`
- JSON-based request/response handling
- **Models** (`models.go`): Type definitions and data structures
- Generic response handling with `Responses[T]` and `Response[T]`
- Domain models: `App`, `AppInstance`, `AppKey`, `CloudletKey`, `Flavor`
- Input types: `NewAppInput`, `NewAppInstanceInput`
- Message interface for error handling
### Configuration Management
- **File-based**: `$HOME/.edge-connect.yaml` (default) or custom via `--config`
- **Environment Variables**: Prefixed with `EDGE_CONNECT_`
- `EDGE_CONNECT_BASE_URL`
- `EDGE_CONNECT_USERNAME`
- `EDGE_CONNECT_PASSWORD`
- **Command-line Flags**: Override other sources
## Dependencies
### Direct Dependencies
- **Cobra v1.10.1**: CLI framework for command structure and parsing
- **Viper v1.21.0**: Configuration management (files, env vars, flags)
### Key Indirect Dependencies
- `fsnotify`: File system watching for config changes
- `go-viper/mapstructure`: Configuration unmarshaling
- `pelletier/go-toml`: TOML configuration support
- Standard Go libraries for HTTP, JSON, system operations
## API Integration
### Authentication Flow
1. Client sends username/password to `/api/v1/login`
2. Receives JWT token in response
3. Token included in `Authorization: Bearer` header for subsequent requests
### API Endpoints
- `/api/v1/auth/ctrl/CreateApp` - Create applications
- `/api/v1/auth/ctrl/ShowApp` - Retrieve applications
- `/api/v1/auth/ctrl/DeleteApp` - Delete applications
- `/api/v1/auth/ctrl/CreateAppInst` - Create instances
- `/api/v1/auth/ctrl/ShowAppInst` - Retrieve instances
- `/api/v1/auth/ctrl/DeleteAppInst` - Delete instances
### Response Handling
- Streaming JSON responses parsed line-by-line
- Generic type-safe response wrapper
- Comprehensive error handling with status codes
- Built-in logging for debugging
## Key Features
### Application Management
- Multi-tenant support with organization scoping
- Version-aware application handling
- Region-based deployments
- Configurable security rules and deployment options
### Instance Management
- Cloudlet-based instance deployment
- Flavor selection for resource allocation
- Application-to-instance relationship tracking
- State and power state monitoring
### Error Handling
- Custom error types (`ErrResourceNotFound`)
- HTTP status code awareness
- Detailed error messages with context
- Graceful handling of missing resources
## Development Notes
### Code Quality
- Clean separation of concerns (CLI/Client/Models)
- Generic programming for type safety
- Consistent error handling patterns
- Comprehensive logging for troubleshooting
### Configuration
- Flexible configuration system supporting multiple sources
- Secure credential handling via environment variables
- Example configuration provided for easy setup
### API Design
- RESTful API integration with structured endpoints
- Token-based security model
- Streaming response handling for efficiency
- Comprehensive swagger specification (370KB)
## Missing Components
- Empty Dockerfile suggests containerization is planned but not implemented
- No tests directory - testing framework needs to be established
- No CI/CD configuration visible
- Limited error recovery and retry mechanisms
## Potential Improvements
1. **Testing**: Implement unit and integration tests
2. **Containerization**: Complete Docker implementation
3. **Retry Logic**: Add resilient API call mechanisms
4. **Configuration Validation**: Validate config before use
5. **Output Formatting**: Add JSON/YAML output options
6. **Caching**: Implement token caching to reduce login calls

214
sdk/client/apps.go Normal file
View file

@ -0,0 +1,214 @@
// ABOUTME: Application lifecycle management APIs for EdgeXR Master Controller
// ABOUTME: Provides typed methods for creating, querying, and deleting applications
package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
)
var (
// ErrResourceNotFound indicates the requested resource was not found
ErrResourceNotFound = fmt.Errorf("resource not found")
)
// CreateApp creates a new application in the specified region
// Maps to POST /auth/ctrl/CreateApp
func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/CreateApp"
resp, err := transport.Call(ctx, "POST", url, input)
if err != nil {
return fmt.Errorf("CreateApp failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateApp")
}
c.logf("CreateApp: %s/%s version %s created successfully",
input.App.Key.Organization, input.App.Key.Name, input.App.Key.Version)
return nil
}
// ShowApp retrieves a single application by key and region
// Maps to POST /auth/ctrl/ShowApp
func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App, error) {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/ShowApp"
filter := AppFilter{
AppKey: appKey,
Region: region,
}
resp, err := transport.Call(ctx, "POST", url, filter)
if err != nil {
return App{}, fmt.Errorf("ShowApp failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
appKey.Organization, appKey.Name, appKey.Version, region, ErrResourceNotFound)
}
if resp.StatusCode >= 400 {
return App{}, c.handleErrorResponse(resp, "ShowApp")
}
// Parse streaming JSON response
var apps []App
if err := c.parseStreamingResponse(resp, &apps); err != nil {
return App{}, fmt.Errorf("ShowApp failed to parse response: %w", err)
}
if len(apps) == 0 {
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
appKey.Organization, appKey.Name, appKey.Version, region, ErrResourceNotFound)
}
return apps[0], nil
}
// ShowApps retrieves all applications matching the filter criteria
// Maps to POST /auth/ctrl/ShowApp
func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]App, error) {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/ShowApp"
filter := AppFilter{
AppKey: appKey,
Region: region,
}
resp, err := transport.Call(ctx, "POST", url, filter)
if err != nil {
return nil, fmt.Errorf("ShowApps failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowApps")
}
var apps []App
if resp.StatusCode == http.StatusNotFound {
return apps, nil // Return empty slice for not found
}
if err := c.parseStreamingResponse(resp, &apps); err != nil {
return nil, fmt.Errorf("ShowApps failed to parse response: %w", err)
}
c.logf("ShowApps: found %d apps matching criteria", len(apps))
return apps, nil
}
// DeleteApp removes an application from the specified region
// Maps to POST /auth/ctrl/DeleteApp
func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) error {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp"
filter := AppFilter{
AppKey: appKey,
Region: region,
}
resp, err := transport.Call(ctx, "POST", url, filter)
if err != nil {
return fmt.Errorf("DeleteApp failed: %w", err)
}
defer resp.Body.Close()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return c.handleErrorResponse(resp, "DeleteApp")
}
c.logf("DeleteApp: %s/%s version %s deleted successfully",
appKey.Organization, appKey.Name, appKey.Version)
return nil
}
// parseStreamingResponse parses the EdgeXR streaming JSON response format
func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) error {
var responses []Response[App]
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
var response Response[App]
if err := json.Unmarshal(line, &response); err != nil {
return err
}
responses = append(responses, response)
return nil
})
if parseErr != nil {
return parseErr
}
// Extract data from responses
var apps []App
var messages []string
for _, response := range responses {
if response.HasData() {
apps = append(apps, response.Data)
}
if response.IsMessage() {
messages = append(messages, response.Data.GetMessage())
}
}
// If we have error messages, return them
if len(messages) > 0 {
return &APIError{
StatusCode: resp.StatusCode,
Messages: messages,
}
}
// Set result based on type
switch v := result.(type) {
case *[]App:
*v = apps
default:
return fmt.Errorf("unsupported result type: %T", result)
}
return nil
}
// getTransport creates an HTTP transport with current client settings
func (c *Client) getTransport() *sdkhttp.Transport {
return sdkhttp.NewTransport(
sdkhttp.RetryOptions{
MaxRetries: c.RetryOpts.MaxRetries,
InitialDelay: c.RetryOpts.InitialDelay,
MaxDelay: c.RetryOpts.MaxDelay,
Multiplier: c.RetryOpts.Multiplier,
RetryableHTTPStatusCodes: c.RetryOpts.RetryableHTTPStatusCodes,
},
c.AuthProvider,
c.Logger,
)
}
// handleErrorResponse creates an appropriate error from HTTP error response
func (c *Client) handleErrorResponse(resp *http.Response, operation string) error {
return &APIError{
StatusCode: resp.StatusCode,
Messages: []string{fmt.Sprintf("%s failed with status %d", operation, resp.StatusCode)},
}
}

319
sdk/client/apps_test.go Normal file
View file

@ -0,0 +1,319 @@
// ABOUTME: Unit tests for App management APIs using httptest mock server
// ABOUTME: Tests create, show, list, and delete operations with error conditions
package client
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateApp(t *testing.T) {
tests := []struct {
name string
input *NewAppInput
mockStatusCode int
mockResponse string
expectError bool
}{
{
name: "successful creation",
input: &NewAppInput{
Region: "us-west",
App: App{
Key: AppKey{
Organization: "testorg",
Name: "testapp",
Version: "1.0.0",
},
Deployment: "kubernetes",
},
},
mockStatusCode: 200,
mockResponse: `{"message": "success"}`,
expectError: false,
},
{
name: "validation error",
input: &NewAppInput{
Region: "us-west",
App: App{
Key: AppKey{
Organization: "",
Name: "testapp",
Version: "1.0.0",
},
},
},
mockStatusCode: 400,
mockResponse: `{"message": "organization is required"}`,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/v1/auth/ctrl/CreateApp", r.URL.Path)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
// Create client
client := NewClient(server.URL,
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
WithAuthProvider(NewStaticTokenProvider("test-token")),
)
// Execute test
ctx := context.Background()
err := client.CreateApp(ctx, tt.input)
// Verify results
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestShowApp(t *testing.T) {
tests := []struct {
name string
appKey AppKey
region string
mockStatusCode int
mockResponse string
expectError bool
expectNotFound bool
}{
{
name: "successful show",
appKey: AppKey{
Organization: "testorg",
Name: "testapp",
Version: "1.0.0",
},
region: "us-west",
mockStatusCode: 200,
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testapp", "version": "1.0.0"}, "deployment": "kubernetes"}}
`,
expectError: false,
expectNotFound: false,
},
{
name: "app not found",
appKey: AppKey{
Organization: "testorg",
Name: "nonexistent",
Version: "1.0.0",
},
region: "us-west",
mockStatusCode: 404,
mockResponse: "",
expectError: true,
expectNotFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/v1/auth/ctrl/ShowApp", r.URL.Path)
w.WriteHeader(tt.mockStatusCode)
if tt.mockResponse != "" {
w.Write([]byte(tt.mockResponse))
}
}))
defer server.Close()
// Create client
client := NewClient(server.URL,
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
)
// Execute test
ctx := context.Background()
app, err := client.ShowApp(ctx, tt.appKey, tt.region)
// Verify results
if tt.expectError {
assert.Error(t, err)
if tt.expectNotFound {
assert.Contains(t, err.Error(), "resource not found")
}
} else {
require.NoError(t, err)
assert.Equal(t, tt.appKey.Organization, app.Key.Organization)
assert.Equal(t, tt.appKey.Name, app.Key.Name)
assert.Equal(t, tt.appKey.Version, app.Key.Version)
}
})
}
}
func TestShowApps(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/v1/auth/ctrl/ShowApp", r.URL.Path)
// Verify request body
var filter AppFilter
err := json.NewDecoder(r.Body).Decode(&filter)
require.NoError(t, err)
assert.Equal(t, "testorg", filter.AppKey.Organization)
assert.Equal(t, "us-west", filter.Region)
// Return multiple apps
response := `{"data": {"key": {"organization": "testorg", "name": "app1", "version": "1.0.0"}, "deployment": "kubernetes"}}
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
`
w.WriteHeader(200)
w.Write([]byte(response))
}))
defer server.Close()
client := NewClient(server.URL)
ctx := context.Background()
apps, err := client.ShowApps(ctx, AppKey{Organization: "testorg"}, "us-west")
require.NoError(t, err)
assert.Len(t, apps, 2)
assert.Equal(t, "app1", apps[0].Key.Name)
assert.Equal(t, "app2", apps[1].Key.Name)
}
func TestDeleteApp(t *testing.T) {
tests := []struct {
name string
appKey AppKey
region string
mockStatusCode int
expectError bool
}{
{
name: "successful deletion",
appKey: AppKey{
Organization: "testorg",
Name: "testapp",
Version: "1.0.0",
},
region: "us-west",
mockStatusCode: 200,
expectError: false,
},
{
name: "already deleted (404 ok)",
appKey: AppKey{
Organization: "testorg",
Name: "testapp",
Version: "1.0.0",
},
region: "us-west",
mockStatusCode: 404,
expectError: false,
},
{
name: "server error",
appKey: AppKey{
Organization: "testorg",
Name: "testapp",
Version: "1.0.0",
},
region: "us-west",
mockStatusCode: 500,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/v1/auth/ctrl/DeleteApp", r.URL.Path)
w.WriteHeader(tt.mockStatusCode)
}))
defer server.Close()
client := NewClient(server.URL)
ctx := context.Background()
err := client.DeleteApp(ctx, tt.appKey, tt.region)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestClientOptions(t *testing.T) {
t.Run("with auth provider", func(t *testing.T) {
authProvider := NewStaticTokenProvider("test-token")
client := NewClient("https://example.com",
WithAuthProvider(authProvider),
)
assert.Equal(t, authProvider, client.AuthProvider)
})
t.Run("with custom HTTP client", func(t *testing.T) {
httpClient := &http.Client{Timeout: 10 * time.Second}
client := NewClient("https://example.com",
WithHTTPClient(httpClient),
)
assert.Equal(t, httpClient, client.HTTPClient)
})
t.Run("with retry options", func(t *testing.T) {
retryOpts := RetryOptions{MaxRetries: 5}
client := NewClient("https://example.com",
WithRetryOptions(retryOpts),
)
assert.Equal(t, 5, client.RetryOpts.MaxRetries)
})
}
func TestAPIError(t *testing.T) {
err := &APIError{
StatusCode: 400,
Messages: []string{"validation failed", "name is required"},
}
assert.Equal(t, "validation failed", err.Error())
assert.Equal(t, 400, err.StatusCode)
assert.Len(t, err.Messages, 2)
}
// Helper function to create a test server that handles streaming JSON responses
func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode)
for _, response := range responses {
w.Write([]byte(response + "\n"))
}
}))
}

46
sdk/client/auth.go Normal file
View file

@ -0,0 +1,46 @@
// ABOUTME: Authentication providers for EdgeXR Master Controller API
// ABOUTME: Supports Bearer token authentication with pluggable provider interface
package client
import (
"context"
"net/http"
)
// AuthProvider interface for attaching authentication to requests
type AuthProvider interface {
// Attach adds authentication headers to the request
Attach(ctx context.Context, req *http.Request) error
}
// StaticTokenProvider implements Bearer token authentication with a fixed token
type StaticTokenProvider struct {
Token string
}
// NewStaticTokenProvider creates a new static token provider
func NewStaticTokenProvider(token string) *StaticTokenProvider {
return &StaticTokenProvider{Token: token}
}
// Attach adds the Bearer token to the request Authorization header
func (s *StaticTokenProvider) Attach(ctx context.Context, req *http.Request) error {
if s.Token != "" {
req.Header.Set("Authorization", "Bearer "+s.Token)
}
return nil
}
// NoAuthProvider implements no authentication (for testing or public endpoints)
type NoAuthProvider struct{}
// NewNoAuthProvider creates a new no-auth provider
func NewNoAuthProvider() *NoAuthProvider {
return &NoAuthProvider{}
}
// Attach does nothing (no authentication)
func (n *NoAuthProvider) Attach(ctx context.Context, req *http.Request) error {
return nil
}

105
sdk/client/client.go Normal file
View file

@ -0,0 +1,105 @@
// ABOUTME: Core EdgeXR Master Controller SDK client with HTTP transport and auth
// ABOUTME: Provides typed APIs for app, instance, and cloudlet management operations
package client
import (
"net/http"
"strings"
"time"
)
// Client represents the EdgeXR Master Controller SDK client
type Client struct {
BaseURL string
HTTPClient *http.Client
AuthProvider AuthProvider
RetryOpts RetryOptions
Logger Logger
}
// RetryOptions configures retry behavior for API calls
type RetryOptions struct {
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
Multiplier float64
RetryableHTTPStatusCodes []int
}
// Logger interface for optional logging
type Logger interface {
Printf(format string, v ...interface{})
}
// DefaultRetryOptions returns sensible default retry configuration
func DefaultRetryOptions() RetryOptions {
return RetryOptions{
MaxRetries: 3,
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0,
RetryableHTTPStatusCodes: []int{
http.StatusRequestTimeout,
http.StatusTooManyRequests,
http.StatusInternalServerError,
http.StatusBadGateway,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout,
},
}
}
// Option represents a configuration option for the client
type Option func(*Client)
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(client *http.Client) Option {
return func(c *Client) {
c.HTTPClient = client
}
}
// WithAuthProvider sets the authentication provider
func WithAuthProvider(auth AuthProvider) Option {
return func(c *Client) {
c.AuthProvider = auth
}
}
// WithRetryOptions sets retry configuration
func WithRetryOptions(opts RetryOptions) Option {
return func(c *Client) {
c.RetryOpts = opts
}
}
// WithLogger sets a logger for debugging
func WithLogger(logger Logger) Option {
return func(c *Client) {
c.Logger = logger
}
}
// NewClient creates a new EdgeXR SDK client
func NewClient(baseURL string, options ...Option) *Client {
client := &Client{
BaseURL: strings.TrimRight(baseURL, "/"),
HTTPClient: &http.Client{Timeout: 30 * time.Second},
AuthProvider: NewNoAuthProvider(),
RetryOpts: DefaultRetryOptions(),
}
for _, opt := range options {
opt(client)
}
return client
}
// logf logs a message if a logger is configured
func (c *Client) logf(format string, v ...interface{}) {
if c.Logger != nil {
c.Logger.Printf(format, v...)
}
}

221
sdk/client/types.go Normal file
View file

@ -0,0 +1,221 @@
// ABOUTME: Core type definitions for EdgeXR Master Controller SDK
// ABOUTME: These types are based on the swagger API specification and existing client patterns
package client
import "time"
// Message interface for types that can provide error messages
type Message interface {
GetMessage() string
}
// Base message type for API responses
type msg struct {
Message string `json:"message,omitempty"`
}
func (m msg) GetMessage() string {
return m.Message
}
// AppKey uniquely identifies an application
type AppKey struct {
Organization string `json:"organization"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}
// CloudletKey uniquely identifies a cloudlet
type CloudletKey struct {
Organization string `json:"organization"`
Name string `json:"name"`
}
// AppInstanceKey uniquely identifies an application instance
type AppInstanceKey struct {
Organization string `json:"organization"`
Name string `json:"name"`
CloudletKey CloudletKey `json:"cloudlet_key"`
}
// Flavor defines resource allocation for instances
type Flavor struct {
Name string `json:"name"`
}
// SecurityRule defines network access rules
type SecurityRule struct {
PortRangeMax int `json:"port_range_max"`
PortRangeMin int `json:"port_range_min"`
Protocol string `json:"protocol"`
RemoteCIDR string `json:"remote_cidr"`
}
// App represents an application definition
type App struct {
msg `json:",inline"`
Key AppKey `json:"key"`
Deployment string `json:"deployment,omitempty"`
ImageType string `json:"image_type,omitempty"`
ImagePath string `json:"image_path,omitempty"`
AllowServerless bool `json:"allow_serverless,omitempty"`
DefaultFlavor Flavor `json:"defaultFlavor,omitempty"`
ServerlessConfig interface{} `json:"serverless_config,omitempty"`
DeploymentGenerator string `json:"deployment_generator,omitempty"`
DeploymentManifest string `json:"deployment_manifest,omitempty"`
RequiredOutboundConnections []SecurityRule `json:"required_outbound_connections"`
}
// AppInstance represents a deployed application instance
type AppInstance struct {
msg `json:",inline"`
Key AppInstanceKey `json:"key"`
AppKey AppKey `json:"app_key,omitempty"`
Flavor Flavor `json:"flavor,omitempty"`
State string `json:"state,omitempty"`
PowerState string `json:"power_state,omitempty"`
}
// Cloudlet represents edge infrastructure
type Cloudlet struct {
msg `json:",inline"`
Key CloudletKey `json:"key"`
Location Location `json:"location"`
IpSupport string `json:"ip_support,omitempty"`
NumDynamicIps int32 `json:"num_dynamic_ips,omitempty"`
State string `json:"state,omitempty"`
Flavor Flavor `json:"flavor,omitempty"`
PhysicalName string `json:"physical_name,omitempty"`
Region string `json:"region,omitempty"`
NotifySrvAddr string `json:"notify_srv_addr,omitempty"`
}
// Location represents geographical coordinates
type Location struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// Input types for API operations
// NewAppInput represents input for creating an application
type NewAppInput struct {
Region string `json:"region"`
App App `json:"app"`
}
// NewAppInstanceInput represents input for creating an app instance
type NewAppInstanceInput struct {
Region string `json:"region"`
AppInst AppInstance `json:"appinst"`
}
// NewCloudletInput represents input for creating a cloudlet
type NewCloudletInput struct {
Region string `json:"region"`
Cloudlet Cloudlet `json:"cloudlet"`
}
// Response wrapper types
// Response wraps a single API response
type Response[T Message] struct {
Data T `json:"data"`
}
func (res *Response[T]) HasData() bool {
return !res.IsMessage()
}
func (res *Response[T]) IsMessage() bool {
return res.Data.GetMessage() != ""
}
// Responses wraps multiple API responses with metadata
type Responses[T Message] struct {
Responses []Response[T] `json:"responses,omitempty"`
StatusCode int `json:"-"`
}
func (r *Responses[T]) GetData() []T {
var data []T
for _, v := range r.Responses {
if v.HasData() {
data = append(data, v.Data)
}
}
return data
}
func (r *Responses[T]) GetMessages() []string {
var messages []string
for _, v := range r.Responses {
if v.IsMessage() {
messages = append(messages, v.Data.GetMessage())
}
}
return messages
}
func (r *Responses[T]) IsSuccessful() bool {
return r.StatusCode >= 200 && r.StatusCode < 400
}
func (r *Responses[T]) Error() error {
if r.IsSuccessful() {
return nil
}
return &APIError{
StatusCode: r.StatusCode,
Messages: r.GetMessages(),
}
}
// APIError represents an API error with details
type APIError struct {
StatusCode int `json:"status_code"`
Code string `json:"code,omitempty"`
Messages []string `json:"messages,omitempty"`
Body []byte `json:"-"`
}
func (e *APIError) Error() string {
if len(e.Messages) > 0 {
return e.Messages[0]
}
return "API error"
}
// Filter types for querying
// AppFilter represents filters for app queries
type AppFilter struct {
AppKey AppKey `json:"app"`
Region string `json:"region"`
}
// AppInstanceFilter represents filters for app instance queries
type AppInstanceFilter struct {
AppInstanceKey AppInstanceKey `json:"appinst"`
Region string `json:"region"`
}
// CloudletFilter represents filters for cloudlet queries
type CloudletFilter struct {
CloudletKey CloudletKey `json:"cloudlet"`
Region string `json:"region"`
}
// CloudletManifest represents cloudlet deployment manifest
type CloudletManifest struct {
Manifest string `json:"manifest"`
LastModified time.Time `json:"last_modified,omitempty"`
}
// CloudletResourceUsage represents cloudlet resource utilization
type CloudletResourceUsage struct {
CloudletKey CloudletKey `json:"cloudlet_key"`
Region string `json:"region"`
Usage map[string]interface{} `json:"usage"`
}

119
sdk/examples/deploy_app.go Normal file
View file

@ -0,0 +1,119 @@
// ABOUTME: Example demonstrating EdgeXR SDK usage for app deployment workflow
// ABOUTME: Shows app creation, querying, and cleanup using the typed SDK APIs
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/client"
)
func main() {
// Configure SDK client
baseURL := getEnvOrDefault("EDGEXR_BASE_URL", "https://hub.apps.edge.platform.mg3.mdb.osc.live/api/v1")
token := getEnvOrDefault("EDGEXR_TOKEN", "")
if token == "" {
log.Fatal("EDGEXR_TOKEN environment variable is required")
}
// Create SDK client with authentication and logging
client := client.NewClient(baseURL,
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
client.WithAuthProvider(client.NewStaticTokenProvider(token)),
client.WithLogger(log.Default()),
)
ctx := context.Background()
// Example application to deploy
app := &client.NewAppInput{
Region: "us-west",
App: client.App{
Key: client.AppKey{
Organization: "myorg",
Name: "my-edge-app",
Version: "1.0.0",
},
Deployment: "kubernetes",
ImageType: "ImageTypeDocker",
ImagePath: "nginx:latest",
DefaultFlavor: client.Flavor{Name: "m4.small"},
},
}
// Demonstrate app lifecycle
if err := demonstrateAppLifecycle(ctx, client, app); err != nil {
log.Fatalf("App lifecycle demonstration failed: %v", err)
}
fmt.Println("✅ SDK example completed successfully!")
}
func demonstrateAppLifecycle(ctx context.Context, c *client.Client, input *client.NewAppInput) error {
appKey := input.App.Key
region := input.Region
fmt.Printf("🚀 Demonstrating EdgeXR SDK with app: %s/%s v%s\n",
appKey.Organization, appKey.Name, appKey.Version)
// Step 1: Create the application
fmt.Println("\n1. Creating application...")
if err := c.CreateApp(ctx, input); err != nil {
return fmt.Errorf("failed to create app: %w", err)
}
fmt.Printf("✅ App created: %s/%s v%s\n", appKey.Organization, appKey.Name, appKey.Version)
// Step 2: Query the application
fmt.Println("\n2. Querying application...")
app, err := c.ShowApp(ctx, appKey, region)
if err != nil {
return fmt.Errorf("failed to show app: %w", err)
}
fmt.Printf("✅ App found: %s/%s v%s (deployment: %s)\n",
app.Key.Organization, app.Key.Name, app.Key.Version, app.Deployment)
// Step 3: List applications in the organization
fmt.Println("\n3. Listing applications...")
filter := client.AppKey{Organization: appKey.Organization}
apps, err := c.ShowApps(ctx, filter, region)
if err != nil {
return fmt.Errorf("failed to list apps: %w", err)
}
fmt.Printf("✅ Found %d applications in organization '%s'\n", len(apps), appKey.Organization)
// Step 4: Clean up - delete the application
fmt.Println("\n4. Cleaning up...")
if err := c.DeleteApp(ctx, appKey, region); err != nil {
return fmt.Errorf("failed to delete app: %w", err)
}
fmt.Printf("✅ App deleted: %s/%s v%s\n", appKey.Organization, appKey.Name, appKey.Version)
// Step 5: Verify deletion
fmt.Println("\n5. Verifying deletion...")
_, err = c.ShowApp(ctx, appKey, region)
if err != nil {
if fmt.Sprintf("%v", err) == client.ErrResourceNotFound.Error() {
fmt.Printf("✅ App successfully deleted (not found)\n")
} else {
return fmt.Errorf("unexpected error verifying deletion: %w", err)
}
} else {
return fmt.Errorf("app still exists after deletion")
}
return nil
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}

View file

@ -0,0 +1,218 @@
// ABOUTME: HTTP transport layer with retry logic and request/response handling
// ABOUTME: Provides resilient HTTP communication with context support and error wrapping
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math"
"math/rand"
"net/http"
"time"
"github.com/hashicorp/go-retryablehttp"
)
// Transport wraps HTTP operations with retry logic and error handling
type Transport struct {
client *retryablehttp.Client
authProvider AuthProvider
logger Logger
}
// AuthProvider interface for attaching authentication
type AuthProvider interface {
Attach(ctx context.Context, req *http.Request) error
}
// Logger interface for request/response logging
type Logger interface {
Printf(format string, v ...interface{})
}
// RetryOptions configures retry behavior
type RetryOptions struct {
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
Multiplier float64
RetryableHTTPStatusCodes []int
}
// NewTransport creates a new HTTP transport with retry capabilities
func NewTransport(opts RetryOptions, auth AuthProvider, logger Logger) *Transport {
client := retryablehttp.NewClient()
// Configure retry policy
client.RetryMax = opts.MaxRetries
client.RetryWaitMin = opts.InitialDelay
client.RetryWaitMax = opts.MaxDelay
// Custom retry policy that considers both network errors and HTTP status codes
client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// Default retry for network errors
if err != nil {
return true, nil
}
// Check if status code is retryable
if resp != nil {
for _, code := range opts.RetryableHTTPStatusCodes {
if resp.StatusCode == code {
return true, nil
}
}
}
return false, nil
}
// Custom backoff with jitter
client.Backoff = func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
mult := math.Pow(opts.Multiplier, float64(attemptNum))
sleep := time.Duration(mult) * min
if sleep > max {
sleep = max
}
// Add jitter
jitter := time.Duration(rand.Float64() * float64(sleep) * 0.1)
return sleep + jitter
}
// Disable default logging if no logger provided
if logger == nil {
client.Logger = nil
}
return &Transport{
client: client,
authProvider: auth,
logger: logger,
}
}
// Call executes an HTTP request with retry logic and returns typed response
func (t *Transport) Call(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
// Marshal request body if provided
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewReader(jsonData)
}
// Create retryable request
req, err := retryablehttp.NewRequestWithContext(ctx, method, url, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Add authentication
if t.authProvider != nil {
if err := t.authProvider.Attach(ctx, req.Request); err != nil {
return nil, fmt.Errorf("failed to attach auth: %w", err)
}
}
// Log request
if t.logger != nil {
t.logger.Printf("HTTP %s %s", method, url)
}
// Execute request
resp, err := t.client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP request failed: %w", err)
}
// Log response
if t.logger != nil {
t.logger.Printf("HTTP %s %s -> %d", method, url, resp.StatusCode)
}
return resp, nil
}
// CallJSON executes a request and unmarshals the response into a typed result
func (t *Transport) CallJSON(ctx context.Context, method, url string, body interface{}, result interface{}) (*http.Response, error) {
resp, err := t.Call(ctx, method, url, body)
if err != nil {
return resp, err
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return resp, fmt.Errorf("failed to read response body: %w", err)
}
// For error responses, don't try to unmarshal into result type
if resp.StatusCode >= 400 {
return resp, &HTTPError{
StatusCode: resp.StatusCode,
Status: resp.Status,
Body: respBody,
}
}
// Unmarshal successful response
if result != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, result); err != nil {
return resp, fmt.Errorf("failed to unmarshal response: %w", err)
}
}
return resp, nil
}
// HTTPError represents an HTTP error response
type HTTPError struct {
StatusCode int `json:"status_code"`
Status string `json:"status"`
Body []byte `json:"-"`
}
func (e *HTTPError) Error() string {
if len(e.Body) > 0 {
return fmt.Sprintf("HTTP %d %s: %s", e.StatusCode, e.Status, string(e.Body))
}
return fmt.Sprintf("HTTP %d %s", e.StatusCode, e.Status)
}
// IsRetryable returns true if the error indicates a retryable condition
func (e *HTTPError) IsRetryable() bool {
return e.StatusCode >= 500 || e.StatusCode == 429 || e.StatusCode == 408
}
// ParseJSONLines parses streaming JSON response line by line
func ParseJSONLines(body io.Reader, callback func([]byte) error) error {
decoder := json.NewDecoder(body)
for {
var raw json.RawMessage
if err := decoder.Decode(&raw); err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("failed to decode JSON line: %w", err)
}
if err := callback(raw); err != nil {
return err
}
}
return nil
}