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:
parent
a71f35163c
commit
9a06c608b2
32 changed files with 14733 additions and 7 deletions
146
.claude/CLAUDE.md
Normal file
146
.claude/CLAUDE.md
Normal 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.
|
||||||
7
.claude/commands/brainstorm.md
Normal file
7
.claude/commands/brainstorm.md
Normal 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. Let’s 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.
|
||||||
|
|
||||||
|
Here’s the idea:
|
||||||
9
.claude/commands/design-arch.md
Normal file
9
.claude/commands/design-arch.md
Normal 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
|
||||||
26
.claude/commands/do-file-issues.md
Normal file
26
.claude/commands/do-file-issues.md
Normal 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.
|
||||||
10
.claude/commands/do-fix.md
Normal file
10
.claude/commands/do-fix.md
Normal 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.
|
||||||
10
.claude/commands/do-issues.md
Normal file
10
.claude/commands/do-issues.md
Normal 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 you’re 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.
|
||||||
17
.claude/commands/do-plan.md
Normal file
17
.claude/commands/do-plan.md
Normal 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.
|
||||||
9
.claude/commands/do-todo.md
Normal file
9
.claude/commands/do-todo.md
Normal 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.
|
||||||
3
.claude/commands/find-missing-tests.md
Normal file
3
.claude/commands/find-missing-tests.md
Normal 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
|
||||||
10
.claude/commands/gh-issue.md
Normal file
10
.claude/commands/gh-issue.md
Normal 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 #
|
||||||
7
.claude/commands/make-github-issues.md
Normal file
7
.claude/commands/make-github-issues.md
Normal 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.
|
||||||
7
.claude/commands/make-local-issues.md
Normal file
7
.claude/commands/make-local-issues.md
Normal 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.
|
||||||
11
.claude/commands/plan-gh.md
Normal file
11
.claude/commands/plan-gh.md
Normal 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:
|
||||||
9
.claude/commands/plan-tdd.md
Normal file
9
.claude/commands/plan-tdd.md
Normal 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
9
.claude/commands/plan.md
Normal 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:
|
||||||
1
.claude/commands/security-review.md
Normal file
1
.claude/commands/security-review.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Review this code for security vulnerabilities, focusing on:
|
||||||
8
.claude/commands/session-summary.md
Normal file
8
.claude/commands/session-summary.md
Normal 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
23
.claude/commands/setup.md
Normal 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
41
Makefile
Normal 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
12716
api/swagger.json
Normal file
File diff suppressed because it is too large
Load diff
10
go.mod
10
go.mod
|
|
@ -3,15 +3,21 @@ module edp.buildth.ing/DevFW-CICD/edge-connect-client
|
||||||
go 1.25.1
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
|
@ -19,6 +25,8 @@ require (
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // 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
|
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
27
go.sum
|
|
@ -1,6 +1,8 @@
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
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/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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
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/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 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
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=
|
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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
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 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
8
oapi-codegen.yaml
Normal file
8
oapi-codegen.yaml
Normal 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
217
plan.md
Normal 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
157
project.md
Normal 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
214
sdk/client/apps.go
Normal 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
319
sdk/client/apps_test.go
Normal 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
46
sdk/client/auth.go
Normal 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
105
sdk/client/client.go
Normal 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
221
sdk/client/types.go
Normal 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
119
sdk/examples/deploy_app.go
Normal 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
|
||||||
|
}
|
||||||
218
sdk/internal/http/transport.go
Normal file
218
sdk/internal/http/transport.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue