mirror of
https://github.com/rancher/norman.git
synced 2025-09-04 16:50:41 +00:00
More initial dev
This commit is contained in:
@@ -2,3 +2,4 @@
|
|||||||
./.dapper
|
./.dapper
|
||||||
./dist
|
./dist
|
||||||
./.trash-cache
|
./.trash-cache
|
||||||
|
./.idea
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/.idea
|
||||||
/.dapper
|
/.dapper
|
||||||
/bin
|
/bin
|
||||||
/dist
|
/dist
|
||||||
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/norman.iml" filepath="$PROJECT_DIR$/.idea/norman.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
8
.idea/norman.iml
generated
8
.idea/norman.iml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
818
.idea/workspace.xml
generated
818
.idea/workspace.xml
generated
@@ -1,818 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="0ae5c379-7142-48eb-92a0-60817f2a93fb" name="Default" comment="" />
|
|
||||||
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
|
|
||||||
<option name="TRACKING_ENABLED" value="true" />
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="FileEditorManager">
|
|
||||||
<leaf>
|
|
||||||
<file leaf-file-name="validation.go" pinned="false" current-in-tab="true">
|
|
||||||
<entry file="file://$PROJECT_DIR$/handler/validation.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="774">
|
|
||||||
<caret line="43" column="0" lean-forward="false" selection-start-line="43" selection-start-column="0" selection-end-line="43" selection-end-column="0" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="reference.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$PROJECT_DIR$/store/reference.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="72">
|
|
||||||
<caret line="4" column="49" lean-forward="false" selection-start-line="4" selection-start-column="49" selection-end-line="4" selection-end-column="49" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="strings.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/strings/strings.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="203">
|
|
||||||
<caret line="94" column="5" lean-forward="false" selection-start-line="94" selection-start-column="5" selection-end-line="94" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="atoi.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/strconv/atoi.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="203">
|
|
||||||
<caret line="155" column="5" lean-forward="false" selection-start-line="155" selection-start-column="5" selection-end-line="155" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="type.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/reflect/type.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="254">
|
|
||||||
<caret line="90" column="12" lean-forward="true" selection-start-line="90" selection-start-column="12" selection-end-line="90" selection-end-column="12" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="registry.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$PROJECT_DIR$/registry/registry.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="90">
|
|
||||||
<caret line="5" column="4" lean-forward="false" selection-start-line="5" selection-start-column="4" selection-end-line="5" selection-end-column="4" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="schemas.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/catalog/schemas.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="126">
|
|
||||||
<caret line="7" column="5" lean-forward="false" selection-start-line="7" selection-start-column="5" selection-end-line="7" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="types.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/catalog/types.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="630">
|
|
||||||
<caret line="50" column="0" lean-forward="false" selection-start-line="50" selection-start-column="0" selection-end-line="50" selection-end-column="0" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="types.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/v3/types.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="213">
|
|
||||||
<caret line="72" column="4" lean-forward="false" selection-start-line="72" selection-start-column="4" selection-end-line="72" selection-end-column="4" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
<file leaf-file-name="error.go" pinned="false" current-in-tab="false">
|
|
||||||
<entry file="file://$PROJECT_DIR$/httperror/error.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="195">
|
|
||||||
<caret line="52" column="5" lean-forward="false" selection-start-line="52" selection-start-column="5" selection-end-line="52" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</file>
|
|
||||||
</leaf>
|
|
||||||
</component>
|
|
||||||
<component name="FileTemplateManagerImpl">
|
|
||||||
<option name="RECENT_TEMPLATES">
|
|
||||||
<list>
|
|
||||||
<option value="Go File" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="GOROOT" path="$USER_HOME$/.local/go1.8.3" />
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="GoLibraries">
|
|
||||||
<option name="urls">
|
|
||||||
<list>
|
|
||||||
<option value="file://$PROJECT_DIR$/../../../.." />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="IdeDocumentHistory">
|
|
||||||
<option name="CHANGED_PATHS">
|
|
||||||
<list>
|
|
||||||
<option value="$PROJECT_DIR$/handler/browser.go" />
|
|
||||||
<option value="$PROJECT_DIR$/handler/collection.go" />
|
|
||||||
<option value="$PROJECT_DIR$/query/condition.go" />
|
|
||||||
<option value="$PROJECT_DIR$/handler/csrf.go" />
|
|
||||||
<option value="$PROJECT_DIR$/handler/parse_collection.go" />
|
|
||||||
<option value="$PROJECT_DIR$/handler/read_input.go" />
|
|
||||||
<option value="$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/catalog/types.go" />
|
|
||||||
<option value="$PROJECT_DIR$/registry/registry.go" />
|
|
||||||
<option value="$PROJECT_DIR$/httperror/error.go" />
|
|
||||||
<option value="$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/v3/types.go" />
|
|
||||||
<option value="$PROJECT_DIR$/store/reference.go" />
|
|
||||||
<option value="$PROJECT_DIR$/handler/validation.go" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="JsBuildToolGruntFileManager" detection-done="true" sorting="DEFINITION_ORDER" />
|
|
||||||
<component name="JsBuildToolPackageJson" detection-done="true" sorting="DEFINITION_ORDER" />
|
|
||||||
<component name="JsGulpfileManager">
|
|
||||||
<detection-done>true</detection-done>
|
|
||||||
<sorting>DEFINITION_ORDER</sorting>
|
|
||||||
</component>
|
|
||||||
<component name="ProjectFrameBounds" extendedState="6">
|
|
||||||
<option name="x" value="-1" />
|
|
||||||
<option name="width" value="1370" />
|
|
||||||
<option name="height" value="769" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectInspectionProfilesVisibleTreeState">
|
|
||||||
<entry key="Project Default">
|
|
||||||
<profile-state>
|
|
||||||
<expanded-state>
|
|
||||||
<State>
|
|
||||||
<id />
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Abstraction issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Assignment issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Bitwise operation issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Class metricsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Class structureJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Cloning issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Code maturity issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Code style issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Compiler issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Concurrency annotation issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Control flow issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Data flow issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Declaration redundancyJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Dependency issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Encapsulation issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Error handlingJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Finalization issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>GPath inspectionsGroovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>General</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>GeneralJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Groovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>ImportsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Inheritance issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Initialization issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Internationalization issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>J2ME issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>JUnit issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Java</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Java 5Java language level migration aidsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Java 7Java language level migration aidsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Java 8Java language level migration aidsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Java language level issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Java language level migration aidsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>JavaBeans issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>JavaFX</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Javadoc issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Kotlin</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Language Injection</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Logging issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Manifest</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Memory issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Method metricsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Modularization issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Naming ConventionsGroovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Naming conventionsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Numeric issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Packaging issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Performance issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Portability issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Potentially confusing code constructsGroovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Probable bugsGroovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Probable bugsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Properties Files</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Properties FilesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Resource management issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Security issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Serialization issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>StyleGroovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Threading issuesGroovy</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Threading issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Verbose or redundant code constructsJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>Visibility issuesJava</id>
|
|
||||||
</State>
|
|
||||||
<State>
|
|
||||||
<id>toString() issuesJava</id>
|
|
||||||
</State>
|
|
||||||
</expanded-state>
|
|
||||||
</profile-state>
|
|
||||||
</entry>
|
|
||||||
</component>
|
|
||||||
<component name="ProjectView">
|
|
||||||
<navigator currentView="ProjectPane" proportions="" version="1">
|
|
||||||
<flattenPackages />
|
|
||||||
<showMembers />
|
|
||||||
<showModules />
|
|
||||||
<showLibraryContents />
|
|
||||||
<hideEmptyPackages />
|
|
||||||
<abbreviatePackageNames />
|
|
||||||
<autoscrollToSource />
|
|
||||||
<autoscrollFromSource ProjectPane="true" />
|
|
||||||
<sortByType />
|
|
||||||
<manualOrder />
|
|
||||||
<foldersAlwaysOnTop value="true" />
|
|
||||||
</navigator>
|
|
||||||
<panes>
|
|
||||||
<pane id="ProjectPane">
|
|
||||||
<subPane>
|
|
||||||
<expand>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="handler" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="handler" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="validation.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="httperror" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="httperror" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="error.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="registry" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="registry" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="registry.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="store" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="store" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="reference.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="catalog" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="catalog" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="schemas.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="catalog" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="types.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="generator" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="v3" type="462c0819:PsiDirectoryNode" />
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<item name="norman" type="b2602c69:ProjectViewProjectNode" />
|
|
||||||
<item name="norman" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="vendor" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="github.com" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="go-rancher" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="v3" type="462c0819:PsiDirectoryNode" />
|
|
||||||
<item name="types.go" type="e9541c9c:GoTreeStructureProvider$GoFileNode" />
|
|
||||||
</path>
|
|
||||||
</expand>
|
|
||||||
<select />
|
|
||||||
</subPane>
|
|
||||||
</pane>
|
|
||||||
<pane id="Scratches" />
|
|
||||||
<pane id="Scope" />
|
|
||||||
</panes>
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent">
|
|
||||||
<property name="settings.editor.selected.configurable" value="com.goide.configuration.GoLibrariesConfigurableProvider" />
|
|
||||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
|
||||||
<property name="go.vendoring.notification.had.been.shown" value="true" />
|
|
||||||
<property name="configurable.Global.GOPATH.is.expanded" value="true" />
|
|
||||||
<property name="configurable.Project.GOPATH.is.expanded" value="true" />
|
|
||||||
<property name="configurable.Module.GOPATH.is.expanded" value="false" />
|
|
||||||
<property name="DefaultGoTemplateProperty" value="Go File" />
|
|
||||||
</component>
|
|
||||||
<component name="RecentsManager">
|
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
|
||||||
<recent name="$PROJECT_DIR$" />
|
|
||||||
</key>
|
|
||||||
</component>
|
|
||||||
<component name="RunDashboard">
|
|
||||||
<option name="ruleStates">
|
|
||||||
<list>
|
|
||||||
<RuleState>
|
|
||||||
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
|
|
||||||
</RuleState>
|
|
||||||
<RuleState>
|
|
||||||
<option name="name" value="StatusDashboardGroupingRule" />
|
|
||||||
</RuleState>
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="RunManager">
|
|
||||||
<configuration name="<template>" type="TestNG" default="true" selected="false">
|
|
||||||
<option name="MAIN_CLASS_NAME" />
|
|
||||||
<option name="VM_PARAMETERS" value="-ea" />
|
|
||||||
<option name="PARAMETERS" />
|
|
||||||
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
|
|
||||||
</configuration>
|
|
||||||
<configuration name="<template>" type="#org.jetbrains.idea.devkit.run.PluginConfigurationType" default="true" selected="false">
|
|
||||||
<option name="VM_PARAMETERS" value="-Xmx512m -Xms256m -XX:MaxPermSize=250m -ea" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
<component name="ShelveChangesManager" show_recycled="false">
|
|
||||||
<option name="remove_strategy" value="false" />
|
|
||||||
</component>
|
|
||||||
<component name="ToolWindowManager">
|
|
||||||
<frame x="-1" y="0" width="1922" height="1081" extended-state="6" />
|
|
||||||
<editor active="true" />
|
|
||||||
<layout>
|
|
||||||
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
|
|
||||||
<window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="true" content_ui="tabs" />
|
|
||||||
<window_info id="Database" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.32983193" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.32983193" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="-1" side_tool="true" content_ui="tabs" />
|
|
||||||
<window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
|
|
||||||
<window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
|
||||||
</layout>
|
|
||||||
<layout-to-restore>
|
|
||||||
<window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="9" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="true" content_ui="tabs" />
|
|
||||||
<window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="8" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.32983193" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="combo" />
|
|
||||||
<window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="3" side_tool="false" content_ui="combo" />
|
|
||||||
<window_info id="Database" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.32983193" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
|
|
||||||
<window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="true" content_ui="tabs" />
|
|
||||||
</layout-to-restore>
|
|
||||||
</component>
|
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
|
||||||
<option name="version" value="1" />
|
|
||||||
</component>
|
|
||||||
<component name="XDebuggerManager">
|
|
||||||
<breakpoint-manager />
|
|
||||||
<watches-manager />
|
|
||||||
</component>
|
|
||||||
<component name="editorHistoryManager">
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/v3/common.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="-162">
|
|
||||||
<caret line="27" column="42" lean-forward="false" selection-start-line="27" selection-start-column="42" selection-end-line="27" selection-end-column="42" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/generator/generator.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="360">
|
|
||||||
<caret line="29" column="36" lean-forward="false" selection-start-line="29" selection-start-column="36" selection-end-line="29" selection-end-column="36" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/v3/generated_ulimit.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="882">
|
|
||||||
<caret line="68" column="0" lean-forward="false" selection-start-line="68" selection-start-column="0" selection-end-line="68" selection-end-column="0" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/io/io.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="308">
|
|
||||||
<caret line="417" column="5" lean-forward="false" selection-start-line="417" selection-start-column="5" selection-end-line="417" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/io/ioutil/ioutil.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="720">
|
|
||||||
<caret line="40" column="5" lean-forward="false" selection-start-line="40" selection-start-column="5" selection-end-line="40" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/net/http/header.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="702">
|
|
||||||
<caret line="39" column="16" lean-forward="false" selection-start-line="39" selection-start-column="16" selection-end-line="39" selection-end-column="16" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/net/http/server.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="-507">
|
|
||||||
<caret line="89" column="5" lean-forward="false" selection-start-line="89" selection-start-column="5" selection-end-line="89" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/net/http/request.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="203">
|
|
||||||
<caret line="372" column="18" lean-forward="false" selection-start-line="372" selection-start-column="18" selection-end-line="372" selection-end-column="18" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/net/url/url.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="308">
|
|
||||||
<caret line="780" column="16" lean-forward="false" selection-start-line="780" selection-start-column="16" selection-end-line="780" selection-end-column="16" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/query/condition.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="468">
|
|
||||||
<caret line="32" column="31" lean-forward="false" selection-start-line="32" selection-start-column="31" selection-end-line="32" selection-end-column="31" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/handler/browser.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="126">
|
|
||||||
<caret line="7" column="53" lean-forward="false" selection-start-line="7" selection-start-column="53" selection-end-line="7" selection-end-column="53" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/handler/csrf.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="216">
|
|
||||||
<caret line="15" column="40" lean-forward="false" selection-start-line="15" selection-start-column="40" selection-end-line="15" selection-end-column="40" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/handler/read_input.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="630">
|
|
||||||
<caret line="35" column="19" lean-forward="false" selection-start-line="35" selection-start-column="19" selection-end-line="35" selection-end-column="19" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/handler/parse_collection.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="324">
|
|
||||||
<caret line="104" column="5" lean-forward="false" selection-start-line="104" selection-start-column="5" selection-end-line="104" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/catalog/types.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="630">
|
|
||||||
<caret line="50" column="0" lean-forward="false" selection-start-line="50" selection-start-column="0" selection-end-line="50" selection-end-column="0" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/catalog/schemas.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="126">
|
|
||||||
<caret line="7" column="5" lean-forward="false" selection-start-line="7" selection-start-column="5" selection-end-line="7" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/reflect/type.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="254">
|
|
||||||
<caret line="90" column="12" lean-forward="true" selection-start-line="90" selection-start-column="12" selection-end-line="90" selection-end-column="12" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/registry/registry.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="90">
|
|
||||||
<caret line="5" column="4" lean-forward="false" selection-start-line="5" selection-start-column="4" selection-end-line="5" selection-end-column="4" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/strconv/atoi.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="203">
|
|
||||||
<caret line="155" column="5" lean-forward="false" selection-start-line="155" selection-start-column="5" selection-end-line="155" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$USER_HOME$/.local/go1.8.3/src/strings/strings.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="203">
|
|
||||||
<caret line="94" column="5" lean-forward="false" selection-start-line="94" selection-start-column="5" selection-end-line="94" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/httperror/error.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="195">
|
|
||||||
<caret line="52" column="5" lean-forward="false" selection-start-line="52" selection-start-column="5" selection-end-line="52" selection-end-column="5" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/vendor/github.com/rancher/go-rancher/v3/types.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="213">
|
|
||||||
<caret line="72" column="4" lean-forward="false" selection-start-line="72" selection-start-column="4" selection-end-line="72" selection-end-column="4" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/store/reference.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="72">
|
|
||||||
<caret line="4" column="49" lean-forward="false" selection-start-line="4" selection-start-column="49" selection-end-line="4" selection-end-column="49" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
<entry file="file://$PROJECT_DIR$/handler/validation.go">
|
|
||||||
<provider selected="true" editor-type-id="text-editor">
|
|
||||||
<state relative-caret-position="774">
|
|
||||||
<caret line="43" column="0" lean-forward="false" selection-start-line="43" selection-start-column="0" selection-end-line="43" selection-end-column="0" />
|
|
||||||
<folding />
|
|
||||||
</state>
|
|
||||||
</provider>
|
|
||||||
</entry>
|
|
||||||
</component>
|
|
||||||
<component name="masterDetails">
|
|
||||||
<states>
|
|
||||||
<state key="ProjectJDKs.UI">
|
|
||||||
<settings>
|
|
||||||
<splitter-proportions>
|
|
||||||
<option name="proportions">
|
|
||||||
<list>
|
|
||||||
<option value="0.2" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</splitter-proportions>
|
|
||||||
</settings>
|
|
||||||
</state>
|
|
||||||
</states>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
64
README.md
64
README.md
@@ -1,19 +1,73 @@
|
|||||||
norman
|
Norman
|
||||||
========
|
========
|
||||||
|
|
||||||
A microservice that does micro things.
|
An API framework for Building [Rancher Style APIs](https://github.com/rancher/api-spec/) backed by K8s CustomResources.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
`make`
|
`make`
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
## Running
|
Refer to `examples/`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rancher/go-rancher/v3"
|
||||||
|
"github.com/rancher/norman/api/crd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = client.APIVersion{
|
||||||
|
Version: "v1",
|
||||||
|
Group: "io.cattle.core.example",
|
||||||
|
Path: "/example/v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
Foo = client.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Version: version,
|
||||||
|
ResourceFields: map[string]*client.Field{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
Create: true,
|
||||||
|
Update: true,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
Create: true,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Schemas = client.NewSchemas().
|
||||||
|
AddSchema(&Foo)
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
server, err := crd.NewAPIServer(context.Background(), os.Getenv("KUBECONFIG"), Schemas)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Listening on 0.0.0.0:1234")
|
||||||
|
http.ListenAndServe("0.0.0.0:1234", server)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
`./bin/norman`
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Copyright (c) 2014-2016 [Rancher Labs, Inc.](http://rancher.com)
|
Copyright (c) 2014-2017 [Rancher Labs, Inc.](http://rancher.com)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
186
api/builtin/builtin.go
Normal file
186
api/builtin/builtin.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package builtin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/store/empty"
|
||||||
|
"github.com/rancher/norman/store/schema"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = types.APIVersion{
|
||||||
|
Group: "io.cattle.builtin",
|
||||||
|
Version: "v3",
|
||||||
|
Path: "/v3",
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema = types.Schema{
|
||||||
|
ID: "schema",
|
||||||
|
Version: Version,
|
||||||
|
CollectionMethods: []string{"GET"},
|
||||||
|
ResourceMethods: []string{"GET"},
|
||||||
|
ResourceFields: map[string]types.Field{
|
||||||
|
"collectionActions": {Type: "map[json]"},
|
||||||
|
"collectionFields": {Type: "map[json]"},
|
||||||
|
"collectionFitlers": {Type: "map[json]"},
|
||||||
|
"collectionMethods": {Type: "array[string]"},
|
||||||
|
"pluralName": {Type: "string"},
|
||||||
|
"resourceActions": {Type: "map[json]"},
|
||||||
|
"resourceFields": {Type: "map[json]"},
|
||||||
|
"resourceMethods": {Type: "array[string]"},
|
||||||
|
"version": {Type: "map[json]"},
|
||||||
|
},
|
||||||
|
Formatter: SchemaFormatter,
|
||||||
|
Store: schema.NewSchemaStore(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Error = types.Schema{
|
||||||
|
ID: "error",
|
||||||
|
Version: Version,
|
||||||
|
ResourceMethods: []string{},
|
||||||
|
CollectionMethods: []string{},
|
||||||
|
ResourceFields: map[string]types.Field{
|
||||||
|
"code": {Type: "string"},
|
||||||
|
"detail": {Type: "string"},
|
||||||
|
"message": {Type: "string"},
|
||||||
|
"status": {Type: "int"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection = types.Schema{
|
||||||
|
ID: "error",
|
||||||
|
Version: Version,
|
||||||
|
ResourceMethods: []string{},
|
||||||
|
CollectionMethods: []string{},
|
||||||
|
ResourceFields: map[string]types.Field{
|
||||||
|
"data": {Type: "array[json]"},
|
||||||
|
"pagination": {Type: "map[json]"},
|
||||||
|
"sort": {Type: "map[json]"},
|
||||||
|
"filters": {Type: "map[json]"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
APIRoot = types.Schema{
|
||||||
|
ID: "apiRoot",
|
||||||
|
Version: Version,
|
||||||
|
ResourceMethods: []string{},
|
||||||
|
CollectionMethods: []string{},
|
||||||
|
ResourceFields: map[string]types.Field{
|
||||||
|
"apiVersion": {Type: "map[json]"},
|
||||||
|
"path": {Type: "string"},
|
||||||
|
},
|
||||||
|
Formatter: APIRootFormatter,
|
||||||
|
Store: NewAPIRootStore(nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
Schemas = types.NewSchemas().
|
||||||
|
AddSchema(&Schema).
|
||||||
|
AddSchema(&Error).
|
||||||
|
AddSchema(&Collection).
|
||||||
|
AddSchema(&APIRoot)
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiVersionFromMap(apiVersion map[string]interface{}) types.APIVersion {
|
||||||
|
path, _ := apiVersion["path"].(string)
|
||||||
|
version, _ := apiVersion["version"].(string)
|
||||||
|
group, _ := apiVersion["group"].(string)
|
||||||
|
|
||||||
|
return types.APIVersion{
|
||||||
|
Path: path,
|
||||||
|
Version: version,
|
||||||
|
Group: group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SchemaFormatter(apiContext *types.APIContext, resource *types.RawResource) {
|
||||||
|
data, _ := resource.Values["version"].(map[string]interface{})
|
||||||
|
apiVersion := apiVersionFromMap(data)
|
||||||
|
|
||||||
|
schema := apiContext.Schemas.Schema(&apiVersion, resource.ID)
|
||||||
|
collectionLink := getSchemaCollectionLink(apiContext, schema)
|
||||||
|
if collectionLink != "" {
|
||||||
|
resource.Links["collection"] = collectionLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSchemaCollectionLink(apiContext *types.APIContext, schema *types.Schema) string {
|
||||||
|
if schema != nil && contains(schema.CollectionMethods, http.MethodGet) {
|
||||||
|
return apiContext.URLBuilder.Collection(schema)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(list []string, needle string) bool {
|
||||||
|
for _, v := range list {
|
||||||
|
if v == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func APIRootFormatter(apiContext *types.APIContext, resource *types.RawResource) {
|
||||||
|
path, _ := resource.Values["path"].(string)
|
||||||
|
if path == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.Links["root"] = apiContext.URLBuilder.RelativeToRoot(path)
|
||||||
|
|
||||||
|
data, _ := resource.Values["apiVersion"].(map[string]interface{})
|
||||||
|
apiVersion := apiVersionFromMap(data)
|
||||||
|
|
||||||
|
for name, schema := range apiContext.Schemas.SchemasForVersion(apiVersion) {
|
||||||
|
collectionLink := getSchemaCollectionLink(apiContext, schema)
|
||||||
|
if collectionLink != "" {
|
||||||
|
resource.Links[name] = collectionLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIRootStore struct {
|
||||||
|
empty.Store
|
||||||
|
roots []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIRootStore(roots []string) types.Store {
|
||||||
|
return &APIRootStore{roots: roots}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIRootStore) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||||
|
for _, version := range apiContext.Schemas.Versions() {
|
||||||
|
if version.Path == id {
|
||||||
|
return apiVersionToAPIRootMap(version), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIRootStore) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||||
|
roots := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, version := range apiContext.Schemas.Versions() {
|
||||||
|
roots = append(roots, apiVersionToAPIRootMap(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, root := range a.roots {
|
||||||
|
roots = append(roots, map[string]interface{}{
|
||||||
|
"path": root,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiVersionToAPIRootMap(version types.APIVersion) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"type": "/v3/apiRoot",
|
||||||
|
"apiVersion": map[string]interface{}{
|
||||||
|
"version": version.Version,
|
||||||
|
"group": version.Group,
|
||||||
|
"path": version.Path,
|
||||||
|
},
|
||||||
|
"path": version.Path,
|
||||||
|
}
|
||||||
|
}
|
31
api/handlers/create.go
Normal file
31
api/handlers/create.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateHandler(request *types.APIContext) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
validator := request.Schema.Validator
|
||||||
|
if validator != nil {
|
||||||
|
if err := validator(request, request.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := request.Body
|
||||||
|
|
||||||
|
store := request.Schema.Store
|
||||||
|
if store != nil {
|
||||||
|
data, err = store.Create(request, request.Schema, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.WriteResponse(http.StatusCreated, data)
|
||||||
|
return nil
|
||||||
|
}
|
20
api/handlers/delete.go
Normal file
20
api/handlers/delete.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteHandler(request *types.APIContext) error {
|
||||||
|
store := request.Schema.Store
|
||||||
|
if store != nil {
|
||||||
|
err := store.Delete(request, request.Schema, request.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.WriteResponse(http.StatusOK, nil)
|
||||||
|
return nil
|
||||||
|
}
|
36
api/handlers/list.go
Normal file
36
api/handlers/list.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/parse"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListHandler(request *types.APIContext) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
data interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
store := request.Schema.Store
|
||||||
|
if store == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.ID == "" {
|
||||||
|
request.QueryOptions = parse.QueryOptions(request.Request, request.Schema)
|
||||||
|
data, err = store.List(request, request.Schema, request.QueryOptions)
|
||||||
|
} else if request.Link == "" {
|
||||||
|
data, err = store.ByID(request, request.Schema, request.ID)
|
||||||
|
} else {
|
||||||
|
return request.Schema.LinkHandler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.WriteResponse(http.StatusOK, data)
|
||||||
|
return nil
|
||||||
|
}
|
30
api/handlers/update.go
Normal file
30
api/handlers/update.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateHandler(request *types.APIContext) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
validator := request.Schema.Validator
|
||||||
|
if validator != nil {
|
||||||
|
if err := validator(request, request.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := request.Body
|
||||||
|
store := request.Schema.Store
|
||||||
|
if store != nil {
|
||||||
|
data, err = store.Update(request, request.Schema, data, request.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.WriteResponse(http.StatusOK, data)
|
||||||
|
return nil
|
||||||
|
}
|
25
api/headers.go
Normal file
25
api/headers.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/norman/api/builtin"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addCommonResponseHeader(apiContext *types.APIContext) error {
|
||||||
|
addExpires(apiContext)
|
||||||
|
return addSchemasHeader(apiContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSchemasHeader(apiContext *types.APIContext) error {
|
||||||
|
schema := apiContext.Schemas.Schema(&builtin.Version, "schema")
|
||||||
|
if schema == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
apiContext.Response.Header().Set("X-Api-Schemas", apiContext.URLBuilder.Collection(schema))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExpires(apiContext *types.APIContext) {
|
||||||
|
apiContext.Response.Header().Set("Expires", "Wed 24 Feb 1982 18:42:00 GMT")
|
||||||
|
}
|
209
api/server.go
Normal file
209
api/server.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/api/builtin"
|
||||||
|
"github.com/rancher/norman/api/handlers"
|
||||||
|
"github.com/rancher/norman/api/writer"
|
||||||
|
"github.com/rancher/norman/authorization"
|
||||||
|
"github.com/rancher/norman/httperror"
|
||||||
|
"github.com/rancher/norman/parse"
|
||||||
|
"github.com/rancher/norman/parse/builder"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser func(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
IgnoreBuiltin bool
|
||||||
|
Parser Parser
|
||||||
|
ResponseWriters map[string]ResponseWriter
|
||||||
|
schemas *types.Schemas
|
||||||
|
Defaults Defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
type Defaults struct {
|
||||||
|
ActionHandler types.ActionHandler
|
||||||
|
ListHandler types.RequestHandler
|
||||||
|
LinkHandler types.RequestHandler
|
||||||
|
CreateHandler types.RequestHandler
|
||||||
|
DeleteHandler types.RequestHandler
|
||||||
|
UpdateHandler types.RequestHandler
|
||||||
|
Store types.Store
|
||||||
|
ErrorHandler types.ErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIServer() *Server {
|
||||||
|
s := &Server{
|
||||||
|
schemas: types.NewSchemas(),
|
||||||
|
ResponseWriters: map[string]ResponseWriter{
|
||||||
|
"json": &writer.JSONResponseWriter{},
|
||||||
|
"html": &writer.HTMLResponseWriter{},
|
||||||
|
},
|
||||||
|
Defaults: Defaults{
|
||||||
|
CreateHandler: handlers.CreateHandler,
|
||||||
|
DeleteHandler: handlers.DeleteHandler,
|
||||||
|
UpdateHandler: handlers.UpdateHandler,
|
||||||
|
ListHandler: handlers.ListHandler,
|
||||||
|
LinkHandler: func(*types.APIContext) error {
|
||||||
|
return httperror.NewAPIError(httperror.NOT_FOUND, "Link not found")
|
||||||
|
},
|
||||||
|
ErrorHandler: httperror.ErrorHandler,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Parser = func(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error) {
|
||||||
|
ctx, err := parse.Parse(rw, req, s.schemas)
|
||||||
|
ctx.ResponseWriter = s.ResponseWriters[ctx.ResponseFormat]
|
||||||
|
if ctx.ResponseWriter == nil {
|
||||||
|
ctx.ResponseWriter = s.ResponseWriters["json"]
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.AccessControl = &authorization.AllAccess{}
|
||||||
|
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start(ctx context.Context) error {
|
||||||
|
return s.addBuiltins(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) AddSchemas(schemas *types.Schemas) error {
|
||||||
|
if schemas.Err() != nil {
|
||||||
|
return schemas.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, schema := range schemas.Schemas() {
|
||||||
|
s.setupDefaults(schema)
|
||||||
|
s.schemas.AddSchema(schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.schemas.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) setupDefaults(schema *types.Schema) {
|
||||||
|
if schema.ActionHandler == nil {
|
||||||
|
schema.ActionHandler = s.Defaults.ActionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.Store == nil {
|
||||||
|
schema.Store = s.Defaults.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.ListHandler == nil {
|
||||||
|
schema.ListHandler = s.Defaults.ListHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.LinkHandler == nil {
|
||||||
|
schema.LinkHandler = s.Defaults.LinkHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.CreateHandler == nil {
|
||||||
|
schema.CreateHandler = s.Defaults.CreateHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.UpdateHandler == nil {
|
||||||
|
schema.UpdateHandler = s.Defaults.UpdateHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.DeleteHandler == nil {
|
||||||
|
schema.DeleteHandler = s.Defaults.DeleteHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.ErrorHandler == nil {
|
||||||
|
schema.ErrorHandler = s.Defaults.ErrorHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if apiResponse, err := s.handle(rw, req); err != nil {
|
||||||
|
s.handleError(apiResponse, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handle(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error) {
|
||||||
|
apiRequest, err := s.Parser(rw, req)
|
||||||
|
if err != nil {
|
||||||
|
return apiRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CheckCSRF(rw, req); err != nil {
|
||||||
|
return apiRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addCommonResponseHeader(apiRequest); err != nil {
|
||||||
|
return apiRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := ValidateAction(apiRequest)
|
||||||
|
if err != nil {
|
||||||
|
return apiRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiRequest.Schema == nil {
|
||||||
|
return apiRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := builder.NewBuilder(apiRequest)
|
||||||
|
|
||||||
|
if action == nil && apiRequest.Type != "" {
|
||||||
|
var handler types.RequestHandler
|
||||||
|
switch apiRequest.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
handler = apiRequest.Schema.ListHandler
|
||||||
|
apiRequest.Body = nil
|
||||||
|
case http.MethodPost:
|
||||||
|
handler = apiRequest.Schema.CreateHandler
|
||||||
|
apiRequest.Body, err = b.Construct(apiRequest.Schema, apiRequest.Body, builder.Create)
|
||||||
|
case http.MethodPut:
|
||||||
|
handler = apiRequest.Schema.UpdateHandler
|
||||||
|
apiRequest.Body, err = b.Construct(apiRequest.Schema, apiRequest.Body, builder.Update)
|
||||||
|
case http.MethodDelete:
|
||||||
|
handler = apiRequest.Schema.DeleteHandler
|
||||||
|
apiRequest.Body = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return apiRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler == nil {
|
||||||
|
return apiRequest, httperror.NewAPIError(httperror.NOT_FOUND, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRequest, handler(apiRequest)
|
||||||
|
} else if action != nil {
|
||||||
|
return apiRequest, handleAction(action, apiRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(action *types.Action, request *types.APIContext) error {
|
||||||
|
return request.Schema.ActionHandler(request.Action, action, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleError(apiRequest *types.APIContext, err error) {
|
||||||
|
if apiRequest.Schema == nil {
|
||||||
|
s.Defaults.ErrorHandler(apiRequest, err)
|
||||||
|
} else if apiRequest.Schema.ErrorHandler != nil {
|
||||||
|
apiRequest.Schema.ErrorHandler(apiRequest, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) addBuiltins(ctx context.Context) error {
|
||||||
|
if s.IgnoreBuiltin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.AddSchemas(builtin.Schemas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
7
api/types.go
Normal file
7
api/types.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "github.com/rancher/norman/types"
|
||||||
|
|
||||||
|
type ResponseWriter interface {
|
||||||
|
Write(apiContext *types.APIContext, code int, obj interface{})
|
||||||
|
}
|
@@ -1,11 +1,14 @@
|
|||||||
package handler
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/rancher/norman/httperror"
|
"github.com/rancher/norman/httperror"
|
||||||
|
"github.com/rancher/norman/parse"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -13,17 +16,42 @@ const (
|
|||||||
csrfHeader = "X-API-CSRF"
|
csrfHeader = "X-API-CSRF"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ValidateAction(request *types.APIContext) (*types.Action, error) {
|
||||||
|
if request.Action == "" || request.Method != http.MethodPost {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := request.Schema.CollectionActions
|
||||||
|
if request.ID != "" {
|
||||||
|
actions = request.Schema.ResourceActions
|
||||||
|
}
|
||||||
|
|
||||||
|
action, ok := actions[request.Action]
|
||||||
|
if !ok {
|
||||||
|
return nil, httperror.NewAPIError(httperror.INVALID_ACTION, fmt.Sprintf("Invalid action: %s", request.Action))
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.ID != "" {
|
||||||
|
resource := request.ReferenceValidator.Lookup(request.Type, request.ID)
|
||||||
|
if resource == nil {
|
||||||
|
return nil, httperror.NewAPIError(httperror.NOT_FOUND, fmt.Sprintf("Failed to find type: %s id: %s", request.Type, request.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := resource.Actions[request.Action]; !ok {
|
||||||
|
return nil, httperror.NewAPIError(httperror.INVALID_ACTION, fmt.Sprintf("Invalid action: %s", request.Action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &action, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CheckCSRF(rw http.ResponseWriter, req *http.Request) error {
|
func CheckCSRF(rw http.ResponseWriter, req *http.Request) error {
|
||||||
if !IsBrowser(req, false) {
|
if !parse.IsBrowser(req, false) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := req.Cookie(csrfCookie)
|
cookie, err := req.Cookie(csrfCookie)
|
||||||
if err != nil {
|
if err == http.ErrNoCookie {
|
||||||
return httperror.NewAPIError(httperror.INVALID_CSRF_TOKEN, "Failed to parse cookies")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cookie == nil {
|
|
||||||
bytes := make([]byte, 5)
|
bytes := make([]byte, 5)
|
||||||
_, err := rand.Read(bytes)
|
_, err := rand.Read(bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,6 +62,8 @@ func CheckCSRF(rw http.ResponseWriter, req *http.Request) error {
|
|||||||
Name: csrfCookie,
|
Name: csrfCookie,
|
||||||
Value: hex.EncodeToString(bytes),
|
Value: hex.EncodeToString(bytes),
|
||||||
}
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return httperror.NewAPIError(httperror.INVALID_CSRF_TOKEN, "Failed to parse cookies")
|
||||||
} else if req.Method != http.MethodGet {
|
} else if req.Method != http.MethodGet {
|
||||||
/*
|
/*
|
||||||
* Very important to use request.getMethod() and not httpRequest.getMethod(). The client can override the HTTP method with _method
|
* Very important to use request.getMethod() and not httpRequest.getMethod(). The client can override the HTTP method with _method
|
46
api/writer/html.go
Normal file
46
api/writer/html.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package writer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
start = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- If you are reading this, there is a good chance you would prefer sending an
|
||||||
|
"Accept: application/json" header and receiving actual JSON responses. -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://releases.rancher.com/api-ui/1.1.0/ui.min.css" />
|
||||||
|
<script src="https://releases.rancher.com/api-ui/1.1.0/ui.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var user = "admin";
|
||||||
|
var curlUser='${CATTLE_ACCESS_KEY}:${CATTLE_SECRET_KEY}';
|
||||||
|
var schemas="%SCHEMAS%";
|
||||||
|
var data =
|
||||||
|
`
|
||||||
|
end = []byte(`</script>
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTMLResponseWriter struct {
|
||||||
|
JSONResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTMLResponseWriter) start(apiContext *types.APIContext, code int, obj interface{}) {
|
||||||
|
apiContext.Response.Header().Set("content-type", "text/html")
|
||||||
|
apiContext.Response.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTMLResponseWriter) Write(apiContext *types.APIContext, code int, obj interface{}) {
|
||||||
|
h.start(apiContext, code, obj)
|
||||||
|
schemaSchema := apiContext.Schemas.Schema(nil, "/v3/schema")
|
||||||
|
if schemaSchema != nil {
|
||||||
|
headerString := strings.Replace(start, "%SCHEMAS%", apiContext.URLBuilder.Collection(schemaSchema), 1)
|
||||||
|
apiContext.Response.Write([]byte(headerString))
|
||||||
|
}
|
||||||
|
h.Body(apiContext, code, obj)
|
||||||
|
if schemaSchema != nil {
|
||||||
|
apiContext.Response.Write(end)
|
||||||
|
}
|
||||||
|
}
|
150
api/writer/json.go
Normal file
150
api/writer/json.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package writer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/parse/builder"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONResponseWriter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONResponseWriter) start(apiContext *types.APIContext, code int, obj interface{}) {
|
||||||
|
apiContext.Response.Header().Set("content-type", "application/json")
|
||||||
|
apiContext.Response.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONResponseWriter) Write(apiContext *types.APIContext, code int, obj interface{}) {
|
||||||
|
j.start(apiContext, code, obj)
|
||||||
|
j.Body(apiContext, code, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONResponseWriter) Body(apiContext *types.APIContext, code int, obj interface{}) {
|
||||||
|
var output interface{}
|
||||||
|
|
||||||
|
builder := builder.NewBuilder(apiContext)
|
||||||
|
|
||||||
|
switch v := obj.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
output = j.writeInterfaceSlice(builder, apiContext, v)
|
||||||
|
case []map[string]interface{}:
|
||||||
|
output = j.writeMapSlice(builder, apiContext, v)
|
||||||
|
case map[string]interface{}:
|
||||||
|
output = j.convert(builder, apiContext, v)
|
||||||
|
case types.RawResource:
|
||||||
|
output = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if output != nil {
|
||||||
|
json.NewEncoder(apiContext.Response).Encode(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (j *JSONResponseWriter) writeMapSlice(builder *builder.Builder, apiContext *types.APIContext, input []map[string]interface{}) *types.GenericCollection {
|
||||||
|
collection := newCollection(apiContext)
|
||||||
|
for _, value := range input {
|
||||||
|
converted := j.convert(builder, apiContext, value)
|
||||||
|
if converted != nil {
|
||||||
|
collection.Data = append(collection.Data, converted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONResponseWriter) writeInterfaceSlice(builder *builder.Builder, apiContext *types.APIContext, input []interface{}) *types.GenericCollection {
|
||||||
|
collection := newCollection(apiContext)
|
||||||
|
for _, value := range input {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
converted := j.convert(builder, apiContext, v)
|
||||||
|
if converted != nil {
|
||||||
|
collection.Data = append(collection.Data, converted)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
collection.Data = append(collection.Data, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(val interface{}) string {
|
||||||
|
if val == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprint(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONResponseWriter) convert(b *builder.Builder, context *types.APIContext, input map[string]interface{}) *types.RawResource {
|
||||||
|
schema := context.Schemas.Schema(context.Version, fmt.Sprint(input["type"]))
|
||||||
|
if schema == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := b.Construct(schema, input, builder.List)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to construct object on output: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawResource := &types.RawResource{
|
||||||
|
ID: toString(input["id"]),
|
||||||
|
Type: schema.ID,
|
||||||
|
Schema: schema,
|
||||||
|
Links: map[string]string{},
|
||||||
|
Actions: map[string]string{},
|
||||||
|
Values: data,
|
||||||
|
ActionLinks: context.Request.Header.Get("X-API-Action-Links") != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
j.addLinks(b, schema, context, input, rawResource)
|
||||||
|
|
||||||
|
if schema.Formatter != nil {
|
||||||
|
schema.Formatter(context, rawResource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawResource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONResponseWriter) addLinks(b *builder.Builder, schema *types.Schema, context *types.APIContext, input map[string]interface{}, rawResource *types.RawResource) {
|
||||||
|
if rawResource.ID != "" {
|
||||||
|
rawResource.Links["self"] = context.URLBuilder.ResourceLink(rawResource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCollection(apiContext *types.APIContext) *types.GenericCollection {
|
||||||
|
result := &types.GenericCollection{
|
||||||
|
Collection: types.Collection{
|
||||||
|
Type: "collection",
|
||||||
|
ResourceType: apiContext.Type,
|
||||||
|
CreateTypes: map[string]string{},
|
||||||
|
Links: map[string]string{
|
||||||
|
"self": apiContext.URLBuilder.Current(),
|
||||||
|
},
|
||||||
|
Actions: map[string]string{},
|
||||||
|
},
|
||||||
|
Data: []interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiContext.Method == http.MethodGet {
|
||||||
|
if apiContext.AccessControl.CanCreate(apiContext.Schema) {
|
||||||
|
result.CreateTypes[apiContext.Schema.ID] = apiContext.URLBuilder.Collection(apiContext.Schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiContext.QueryOptions != nil {
|
||||||
|
result.Sort = &apiContext.QueryOptions.Sort
|
||||||
|
result.Sort.Reverse = apiContext.URLBuilder.ReverseSort(result.Sort.Order)
|
||||||
|
result.Pagination = apiContext.QueryOptions.Pagination
|
||||||
|
result.Filters = map[string][]types.Condition{}
|
||||||
|
|
||||||
|
for _, cond := range apiContext.QueryOptions.Conditions {
|
||||||
|
filters := result.Filters[cond.Field]
|
||||||
|
result.Filters[cond.Field] = append(filters, cond.ToCondition())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
28
authorization/all.go
Normal file
28
authorization/all.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AllAccess struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*AllAccess) CanCreate(schema *types.Schema) bool {
|
||||||
|
for _, method := range schema.CollectionMethods {
|
||||||
|
if method == http.MethodPost {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*AllAccess) CanList(schema *types.Schema) bool {
|
||||||
|
for _, method := range schema.CollectionMethods {
|
||||||
|
if method == http.MethodGet {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
19
clientbase/client.go
Normal file
19
clientbase/client.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package clientbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIBaseClient struct {
|
||||||
|
Ops *APIOperations
|
||||||
|
Opts *ClientOpts
|
||||||
|
Types map[string]types.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIOperations struct {
|
||||||
|
Opts *ClientOpts
|
||||||
|
Types map[string]types.Schema
|
||||||
|
Client *http.Client
|
||||||
|
}
|
291
clientbase/common.go
Normal file
291
clientbase/common.go
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
package clientbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SELF = "self"
|
||||||
|
COLLECTION = "collection"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debug = false
|
||||||
|
dialer = &websocket.Dialer{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientOpts struct {
|
||||||
|
URL string
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
Timeout time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIError struct {
|
||||||
|
StatusCode int
|
||||||
|
URL string
|
||||||
|
Msg string
|
||||||
|
Status string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
return e.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
apiError, ok := err.(*APIError)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiError.StatusCode == http.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApiError(resp *http.Response, url string) *APIError {
|
||||||
|
contents, err := ioutil.ReadAll(resp.Body)
|
||||||
|
var body string
|
||||||
|
if err != nil {
|
||||||
|
body = "Unreadable body."
|
||||||
|
} else {
|
||||||
|
body = string(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{}
|
||||||
|
if json.Unmarshal(contents, &data) == nil {
|
||||||
|
delete(data, "id")
|
||||||
|
delete(data, "links")
|
||||||
|
delete(data, "actions")
|
||||||
|
delete(data, "type")
|
||||||
|
delete(data, "status")
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
for k, v := range data {
|
||||||
|
if v == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(buf, "%s=%v", k, v)
|
||||||
|
}
|
||||||
|
body = buf.String()
|
||||||
|
}
|
||||||
|
formattedMsg := fmt.Sprintf("Bad response statusCode [%d]. Status [%s]. Body: [%s] from [%s]",
|
||||||
|
resp.StatusCode, resp.Status, body, url)
|
||||||
|
return &APIError{
|
||||||
|
URL: url,
|
||||||
|
Msg: formattedMsg,
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Status: resp.Status,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(array []string, item string) bool {
|
||||||
|
for _, check := range array {
|
||||||
|
if check == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendFilters(urlString string, filters map[string]interface{}) (string, error) {
|
||||||
|
if len(filters) == 0 {
|
||||||
|
return urlString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(urlString)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
for k, v := range filters {
|
||||||
|
if l, ok := v.([]string); ok {
|
||||||
|
for _, v := range l {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q.Add(k, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIClient(opts *ClientOpts) (APIBaseClient, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
result := APIBaseClient{
|
||||||
|
Types: map[string]types.Schema{},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := opts.HTTPClient
|
||||||
|
if client == nil {
|
||||||
|
client = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Timeout == 0 {
|
||||||
|
opts.Timeout = time.Second * 10
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Timeout = opts.Timeout
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", opts.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return result, newApiError(resp, opts.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
schemasURLs := resp.Header.Get("X-API-Schemas")
|
||||||
|
if len(schemasURLs) == 0 {
|
||||||
|
return result, errors.New("Failed to find schema at [" + opts.URL + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if schemasURLs != opts.URL {
|
||||||
|
req, err = http.NewRequest("GET", schemasURLs, nil)
|
||||||
|
req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return result, newApiError(resp, opts.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemas types.SchemaCollection
|
||||||
|
bytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bytes, &schemas)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, schema := range schemas.Data {
|
||||||
|
result.Types[schema.ID] = schema
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Opts = opts
|
||||||
|
result.Ops = &APIOperations{
|
||||||
|
Opts: opts,
|
||||||
|
Client: client,
|
||||||
|
Types: result.Types,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListOpts() *types.ListOpts {
|
||||||
|
return &types.ListOpts{
|
||||||
|
Filters: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Websocket(url string, headers map[string][]string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
httpHeaders := http.Header{}
|
||||||
|
for k, v := range httpHeaders {
|
||||||
|
httpHeaders[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Opts != nil {
|
||||||
|
s := a.Opts.AccessKey + ":" + a.Opts.SecretKey
|
||||||
|
httpHeaders.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialer.Dial(url, http.Header(httpHeaders))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) List(schemaType string, opts *types.ListOpts, respObject interface{}) error {
|
||||||
|
return a.Ops.DoList(schemaType, opts, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Post(url string, createObj interface{}, respObject interface{}) error {
|
||||||
|
return a.Ops.DoModify("POST", url, createObj, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) GetLink(resource types.Resource, link string, respObject interface{}) error {
|
||||||
|
url := resource.Links[link]
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("Failed to find link: %s", link)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Ops.DoGet(url, &types.ListOpts{}, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Create(schemaType string, createObj interface{}, respObject interface{}) error {
|
||||||
|
return a.Ops.DoCreate(schemaType, createObj, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Update(schemaType string, existing *types.Resource, updates interface{}, respObject interface{}) error {
|
||||||
|
return a.Ops.DoUpdate(schemaType, existing, updates, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) ById(schemaType string, id string, respObject interface{}) error {
|
||||||
|
return a.Ops.DoById(schemaType, id, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Delete(existing *types.Resource) error {
|
||||||
|
if existing == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.Ops.DoResourceDelete(existing.Type, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Reload(existing *types.Resource, output interface{}) error {
|
||||||
|
selfUrl, ok := existing.Links[SELF]
|
||||||
|
if !ok {
|
||||||
|
return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Ops.DoGet(selfUrl, NewListOpts(), output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIBaseClient) Action(schemaType string, action string,
|
||||||
|
existing *types.Resource, inputObject, respObject interface{}) error {
|
||||||
|
return a.Ops.DoAction(schemaType, action, existing, inputObject, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
debug = os.Getenv("RANCHER_CLIENT_DEBUG") == "true"
|
||||||
|
if debug {
|
||||||
|
fmt.Println("Rancher client debug on")
|
||||||
|
}
|
||||||
|
}
|
184
clientbase/object_client.go
Normal file
184
clientbase/object_client.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package clientbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ObjectFactory interface {
|
||||||
|
Object() runtime.Object
|
||||||
|
List() runtime.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectClient struct {
|
||||||
|
restClient rest.Interface
|
||||||
|
resource *metav1.APIResource
|
||||||
|
gvk schema.GroupVersionKind
|
||||||
|
ns string
|
||||||
|
Factory ObjectFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewObjectClient(namespace string, config rest.Config, apiResource *metav1.APIResource, gvk schema.GroupVersionKind, factory ObjectFactory) (*ObjectClient, error) {
|
||||||
|
if config.NegotiatedSerializer == nil {
|
||||||
|
configConfig := dynamic.ContentConfig()
|
||||||
|
config.NegotiatedSerializer = configConfig.NegotiatedSerializer
|
||||||
|
}
|
||||||
|
|
||||||
|
restClient, err := rest.UnversionedRESTClientFor(&config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ObjectClient{
|
||||||
|
restClient: restClient,
|
||||||
|
resource: apiResource,
|
||||||
|
gvk: gvk,
|
||||||
|
ns: namespace,
|
||||||
|
Factory: factory,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) Create(o runtime.Object) (runtime.Object, error) {
|
||||||
|
ns := p.ns
|
||||||
|
if obj, ok := o.(metav1.Object); ok && obj.GetNamespace() != "" {
|
||||||
|
ns = obj.GetNamespace()
|
||||||
|
}
|
||||||
|
result := p.Factory.Object()
|
||||||
|
err := p.restClient.Post().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
NamespaceIfScoped(ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
Body(o).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) Get(name string, opts metav1.GetOptions) (runtime.Object, error) {
|
||||||
|
result := p.Factory.Object()
|
||||||
|
err := p.restClient.Get().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
NamespaceIfScoped(p.ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
VersionedParams(&opts, dynamic.VersionedParameterEncoderWithV1Fallback).
|
||||||
|
Name(name).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) Update(name string, o runtime.Object) (runtime.Object, error) {
|
||||||
|
ns := p.ns
|
||||||
|
if obj, ok := o.(metav1.Object); ok && obj.GetNamespace() != "" {
|
||||||
|
ns = obj.GetNamespace()
|
||||||
|
}
|
||||||
|
result := p.Factory.Object()
|
||||||
|
if len(name) == 0 {
|
||||||
|
return result, errors.New("object missing name")
|
||||||
|
}
|
||||||
|
err := p.restClient.Put().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
NamespaceIfScoped(ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
Name(name).
|
||||||
|
Body(o).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) Delete(name string, opts *metav1.DeleteOptions) error {
|
||||||
|
return p.restClient.Delete().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
NamespaceIfScoped(p.ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
Name(name).
|
||||||
|
Body(opts).
|
||||||
|
Do().
|
||||||
|
Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) List(opts metav1.ListOptions) (runtime.Object, error) {
|
||||||
|
result := p.Factory.List()
|
||||||
|
return result, p.restClient.Get().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
NamespaceIfScoped(p.ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
VersionedParams(&opts, dynamic.VersionedParameterEncoderWithV1Fallback).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) Watch(opts metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
r, err := p.restClient.Get().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
Prefix("watch").
|
||||||
|
Namespace(p.ns).
|
||||||
|
NamespaceIfScoped(p.ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
VersionedParams(&opts, dynamic.VersionedParameterEncoderWithV1Fallback).
|
||||||
|
Stream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return watch.NewStreamWatcher(&dynamicDecoder{
|
||||||
|
factory: p.Factory,
|
||||||
|
dec: json.NewDecoder(r),
|
||||||
|
close: r.Close,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ObjectClient) DeleteCollection(deleteOptions *metav1.DeleteOptions, listOptions metav1.ListOptions) error {
|
||||||
|
return p.restClient.Delete().
|
||||||
|
Prefix("apis", p.gvk.Group, p.gvk.Version).
|
||||||
|
NamespaceIfScoped(p.ns, p.resource.Namespaced).
|
||||||
|
Resource(p.resource.Name).
|
||||||
|
VersionedParams(&listOptions, dynamic.VersionedParameterEncoderWithV1Fallback).
|
||||||
|
Body(deleteOptions).
|
||||||
|
Do().
|
||||||
|
Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
type dynamicDecoder struct {
|
||||||
|
factory ObjectFactory
|
||||||
|
dec *json.Decoder
|
||||||
|
close func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dynamicDecoder) Close() {
|
||||||
|
d.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dynamicDecoder) Decode() (action watch.EventType, object runtime.Object, err error) {
|
||||||
|
e := dynamicEvent{
|
||||||
|
Object: holder{
|
||||||
|
factory: d.factory,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := d.dec.Decode(&e); err != nil {
|
||||||
|
return watch.Error, nil, err
|
||||||
|
}
|
||||||
|
return e.Type, e.Object.obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dynamicEvent struct {
|
||||||
|
Type watch.EventType
|
||||||
|
Object holder
|
||||||
|
}
|
||||||
|
|
||||||
|
type holder struct {
|
||||||
|
factory ObjectFactory
|
||||||
|
obj runtime.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *holder) UnmarshalJSON(b []byte) error {
|
||||||
|
h.obj = h.factory.Object()
|
||||||
|
return json.Unmarshal(b, h.obj)
|
||||||
|
}
|
313
clientbase/ops.go
Normal file
313
clientbase/ops.go
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
package clientbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *APIOperations) setupRequest(req *http.Request) {
|
||||||
|
req.SetBasicAuth(a.Opts.AccessKey, a.Opts.SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoDelete(url string) error {
|
||||||
|
req, err := http.NewRequest("DELETE", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.setupRequest(req)
|
||||||
|
|
||||||
|
resp, err := a.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
io.Copy(ioutil.Discard, resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return newApiError(resp, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoGet(url string, opts *types.ListOpts, respObject interface{}) error {
|
||||||
|
if opts == nil {
|
||||||
|
opts = NewListOpts()
|
||||||
|
}
|
||||||
|
url, err := appendFilters(url, opts.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Println("GET " + url)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.setupRequest(req)
|
||||||
|
|
||||||
|
resp, err := a.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return newApiError(resp, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
byteContent, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Println("Response <= " + string(byteContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(byteContent, respObject); err != nil {
|
||||||
|
return errors.Wrap(err, fmt.Sprintf("Failed to parse: %s", byteContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoList(schemaType string, opts *types.ListOpts, respObject interface{}) error {
|
||||||
|
schema, ok := a.Types[schemaType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(schema.CollectionMethods, "GET") {
|
||||||
|
return errors.New("Resource type [" + schemaType + "] is not listable")
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionUrl, ok := schema.Links[COLLECTION]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Failed to find collection URL for [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.DoGet(collectionUrl, opts, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoNext(nextUrl string, respObject interface{}) error {
|
||||||
|
return a.DoGet(nextUrl, nil, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoModify(method string, url string, createObj interface{}, respObject interface{}) error {
|
||||||
|
bodyContent, err := json.Marshal(createObj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Println(method + " " + url)
|
||||||
|
fmt.Println("Request => " + string(bodyContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(bodyContent))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.setupRequest(req)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := a.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return newApiError(resp, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
byteContent, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(byteContent) > 0 {
|
||||||
|
if debug {
|
||||||
|
fmt.Println("Response <= " + string(byteContent))
|
||||||
|
}
|
||||||
|
return json.Unmarshal(byteContent, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoCreate(schemaType string, createObj interface{}, respObject interface{}) error {
|
||||||
|
if createObj == nil {
|
||||||
|
createObj = map[string]string{}
|
||||||
|
}
|
||||||
|
if respObject == nil {
|
||||||
|
respObject = &map[string]interface{}{}
|
||||||
|
}
|
||||||
|
schema, ok := a.Types[schemaType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(schema.CollectionMethods, "POST") {
|
||||||
|
return errors.New("Resource type [" + schemaType + "] is not creatable")
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectionUrl string
|
||||||
|
collectionUrl, ok = schema.Links[COLLECTION]
|
||||||
|
if !ok {
|
||||||
|
// return errors.New("Failed to find collection URL for [" + schemaType + "]")
|
||||||
|
// This is a hack to address https://github.com/rancher/cattle/issues/254
|
||||||
|
re := regexp.MustCompile("schemas.*")
|
||||||
|
collectionUrl = re.ReplaceAllString(schema.Links[SELF], schema.PluralName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.DoModify("POST", collectionUrl, createObj, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoUpdate(schemaType string, existing *types.Resource, updates interface{}, respObject interface{}) error {
|
||||||
|
if existing == nil {
|
||||||
|
return errors.New("Existing object is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
selfUrl, ok := existing.Links[SELF]
|
||||||
|
if !ok {
|
||||||
|
return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
|
||||||
|
}
|
||||||
|
|
||||||
|
if updates == nil {
|
||||||
|
updates = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if respObject == nil {
|
||||||
|
respObject = &map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
schema, ok := a.Types[schemaType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(schema.ResourceMethods, "PUT") {
|
||||||
|
return errors.New("Resource type [" + schemaType + "] is not updatable")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.DoModify("PUT", selfUrl, updates, respObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoById(schemaType string, id string, respObject interface{}) error {
|
||||||
|
schema, ok := a.Types[schemaType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(schema.ResourceMethods, "GET") {
|
||||||
|
return errors.New("Resource type [" + schemaType + "] can not be looked up by ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionUrl, ok := schema.Links[COLLECTION]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Failed to find collection URL for [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.DoGet(collectionUrl+"/"+id, nil, respObject)
|
||||||
|
//TODO check for 404 and return nil, nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoResourceDelete(schemaType string, existing *types.Resource) error {
|
||||||
|
schema, ok := a.Types[schemaType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(schema.ResourceMethods, "DELETE") {
|
||||||
|
return errors.New("Resource type [" + schemaType + "] can not be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
selfUrl, ok := existing.Links[SELF]
|
||||||
|
if !ok {
|
||||||
|
return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.DoDelete(selfUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIOperations) DoAction(schemaType string, action string,
|
||||||
|
existing *types.Resource, inputObject, respObject interface{}) error {
|
||||||
|
|
||||||
|
if existing == nil {
|
||||||
|
return errors.New("Existing object is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
actionUrl, ok := existing.Actions[action]
|
||||||
|
if !ok {
|
||||||
|
return errors.New(fmt.Sprintf("Action [%v] not available on [%v]", action, existing))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = a.Types[schemaType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unknown schema type [" + schemaType + "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
var input io.Reader
|
||||||
|
|
||||||
|
if inputObject != nil {
|
||||||
|
bodyContent, err := json.Marshal(inputObject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
fmt.Println("Request => " + string(bodyContent))
|
||||||
|
}
|
||||||
|
input = bytes.NewBuffer(bodyContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", actionUrl, input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.setupRequest(req)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Content-Length", "0")
|
||||||
|
|
||||||
|
resp, err := a.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return newApiError(resp, actionUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
byteContent, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Println("Response <= " + string(byteContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(byteContent, respObject)
|
||||||
|
}
|
152
controller/generic_controller.go
Normal file
152
controller/generic_controller.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/clientbase"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
resyncPeriod = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerFunc func(key string) error
|
||||||
|
|
||||||
|
type GenericController interface {
|
||||||
|
Informer() cache.SharedIndexInformer
|
||||||
|
AddHandler(handler HandlerFunc)
|
||||||
|
Enqueue(namespace, name string)
|
||||||
|
Start(threadiness int, ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type genericController struct {
|
||||||
|
sync.Mutex
|
||||||
|
informer cache.SharedIndexInformer
|
||||||
|
handlers []HandlerFunc
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
name string
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGenericController(name string, objectClient *clientbase.ObjectClient) (GenericController, error) {
|
||||||
|
informer := cache.NewSharedIndexInformer(
|
||||||
|
&cache.ListWatch{
|
||||||
|
ListFunc: objectClient.List,
|
||||||
|
WatchFunc: objectClient.Watch,
|
||||||
|
},
|
||||||
|
objectClient.Factory.Object(), resyncPeriod, cache.Indexers{})
|
||||||
|
|
||||||
|
return &genericController{
|
||||||
|
informer: informer,
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(),
|
||||||
|
name),
|
||||||
|
name: name,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) Informer() cache.SharedIndexInformer {
|
||||||
|
return g.informer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) Enqueue(namespace, name string) {
|
||||||
|
if namespace == "" {
|
||||||
|
g.queue.Add(name)
|
||||||
|
} else {
|
||||||
|
g.queue.Add(namespace + "/" + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) AddHandler(handler HandlerFunc) {
|
||||||
|
g.handlers = append(g.handlers, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) Start(threadiness int, ctx context.Context) error {
|
||||||
|
g.Lock()
|
||||||
|
defer g.Unlock()
|
||||||
|
|
||||||
|
if !g.running {
|
||||||
|
go g.run(threadiness, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.running = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) queueObject(obj interface{}) {
|
||||||
|
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||||
|
if err == nil {
|
||||||
|
g.queue.Add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) run(threadiness int, ctx context.Context) {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
defer g.queue.ShutDown()
|
||||||
|
|
||||||
|
g.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: g.queueObject,
|
||||||
|
UpdateFunc: func(_, obj interface{}) {
|
||||||
|
g.queueObject(obj)
|
||||||
|
},
|
||||||
|
DeleteFunc: g.queueObject,
|
||||||
|
})
|
||||||
|
|
||||||
|
logrus.Infof("Starting %s Controller", g.name)
|
||||||
|
|
||||||
|
go g.informer.Run(ctx.Done())
|
||||||
|
|
||||||
|
if !cache.WaitForCacheSync(ctx.Done(), g.informer.HasSynced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < threadiness; i++ {
|
||||||
|
go wait.Until(g.runWorker, time.Second, ctx.Done())
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
logrus.Infof("Shutting down %s controller", g.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) runWorker() {
|
||||||
|
for g.processNextWorkItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) processNextWorkItem() bool {
|
||||||
|
key, quit := g.queue.Get()
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer g.queue.Done(key)
|
||||||
|
|
||||||
|
// do your work on the key. This method will contains your "do stuff" logic
|
||||||
|
err := g.syncHandler(key.(string))
|
||||||
|
if err == nil {
|
||||||
|
g.queue.Forget(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err))
|
||||||
|
g.queue.AddRateLimited(key)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericController) syncHandler(s string) error {
|
||||||
|
var errs []error
|
||||||
|
for _, handler := range g.handlers {
|
||||||
|
if err := handler(s); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.NewErrors(errs)
|
||||||
|
}
|
51
example/main.go
Normal file
51
example/main.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/generator"
|
||||||
|
"github.com/rancher/norman/server"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Foo struct {
|
||||||
|
types.Resource
|
||||||
|
Name string `json:"name"`
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
SubThing Baz `json:"subThing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Baz struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = types.APIVersion{
|
||||||
|
Version: "v1",
|
||||||
|
Group: "io.cattle.core.example",
|
||||||
|
Path: "/example/v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
Schemas = types.NewSchemas()
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if _, err := Schemas.Import(&version, Foo{}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generator.Generate("example_gen", Schemas); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := server.NewAPIServer(context.Background(), os.Getenv("KUBECONFIG"), Schemas)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Listening on 0.0.0.0:1234")
|
||||||
|
http.ListenAndServe("0.0.0.0:1234", server)
|
||||||
|
}
|
31
generator/client_template.go
Normal file
31
generator/client_template.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
var clientTemplate = `package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/norman/clientbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
clientbase.APIBaseClient
|
||||||
|
|
||||||
|
{{range .schemas}}
|
||||||
|
{{- if . | hasGet }}{{.ID | capitalize}} {{.ID | capitalize}}Operations
|
||||||
|
{{end}}{{end}}}
|
||||||
|
|
||||||
|
func NewClient(opts *clientbase.ClientOpts) (*Client, error) {
|
||||||
|
baseClient, err := clientbase.NewAPIClient(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
APIBaseClient: baseClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
{{range .schemas}}
|
||||||
|
{{- if . | hasGet }}client.{{.ID | capitalize}} = new{{.ID | capitalize}}Client(client)
|
||||||
|
{{end}}{{end}}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
`
|
146
generator/controller_template.go
Normal file
146
generator/controller_template.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
var controllerTemplate = `package {{.schema.Version.Version}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/clientbase"
|
||||||
|
"github.com/rancher/norman/controller"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
{{.schema.CodeName}}GroupVersionKind = schema.GroupVersionKind{
|
||||||
|
Version: "{{.schema.Version.Version}}",
|
||||||
|
Group: "{{.schema.Version.Group}}",
|
||||||
|
Kind: "{{.schema.CodeName}}",
|
||||||
|
}
|
||||||
|
{{.schema.CodeName}}Resource = metav1.APIResource{
|
||||||
|
Name: "{{.schema.PluralName | toLower}}",
|
||||||
|
SingularName: "{{.schema.ID | toLower}}",
|
||||||
|
Namespaced: false,
|
||||||
|
Kind: {{.schema.CodeName}}GroupVersionKind.Kind,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{.schema.CodeName}}HandlerFunc func(key string, obj *{{.schema.CodeName}}) error
|
||||||
|
|
||||||
|
type {{.schema.CodeName}}Controller interface {
|
||||||
|
Informer() cache.SharedIndexInformer
|
||||||
|
AddHandler(handler {{.schema.CodeName}}HandlerFunc)
|
||||||
|
Enqueue(namespace, name string)
|
||||||
|
Start(threadiness int, ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{.schema.CodeName}}Interface interface {
|
||||||
|
Create(*{{.schema.CodeName}}) (*{{.schema.CodeName}}, error)
|
||||||
|
Get(name string, opts metav1.GetOptions) (*{{.schema.CodeName}}, error)
|
||||||
|
Update(*{{.schema.CodeName}}) (*{{.schema.CodeName}}, error)
|
||||||
|
Delete(name string, options *metav1.DeleteOptions) error
|
||||||
|
List(opts metav1.ListOptions) (*{{.schema.CodeName}}List, error)
|
||||||
|
Watch(opts metav1.ListOptions) (watch.Interface, error)
|
||||||
|
DeleteCollection(deleteOpts *metav1.DeleteOptions, listOpts metav1.ListOptions) error
|
||||||
|
Controller() ({{.schema.CodeName}}Controller, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{.schema.ID}}Controller struct {
|
||||||
|
controller.GenericController
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *{{.schema.ID}}Controller) AddHandler(handler {{.schema.CodeName}}HandlerFunc) {
|
||||||
|
c.GenericController.AddHandler(func(key string) error {
|
||||||
|
obj, exists, err := c.Informer().GetStore().GetByKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return handler(key, nil)
|
||||||
|
}
|
||||||
|
return handler(key, obj.(*{{.schema.CodeName}}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{.schema.ID}}Factory struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c {{.schema.ID}}Factory) Object() runtime.Object {
|
||||||
|
return &{{.schema.CodeName}}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c {{.schema.ID}}Factory) List() runtime.Object {
|
||||||
|
return &{{.schema.CodeName}}List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New{{.schema.CodeName}}Client(namespace string, config rest.Config) ({{.schema.CodeName}}Interface, error) {
|
||||||
|
objectClient, err := clientbase.NewObjectClient(namespace, config, &{{.schema.CodeName}}Resource, {{.schema.CodeName}}GroupVersionKind, {{.schema.ID}}Factory{})
|
||||||
|
return &{{.schema.ID}}Client{
|
||||||
|
objectClient: objectClient,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) Controller() ({{.schema.CodeName}}Controller, error) {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if s.controller != nil {
|
||||||
|
return s.controller, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
controller, err := controller.NewGenericController({{.schema.CodeName}}GroupVersionKind.Kind+"Controller",
|
||||||
|
s.objectClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.controller = &{{.schema.ID}}Controller{
|
||||||
|
GenericController: controller,
|
||||||
|
}
|
||||||
|
return s.controller, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{.schema.ID}}Client struct {
|
||||||
|
sync.Mutex
|
||||||
|
objectClient *clientbase.ObjectClient
|
||||||
|
controller {{.schema.CodeName}}Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) Create(o *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error) {
|
||||||
|
obj, err := s.objectClient.Create(o)
|
||||||
|
return obj.(*{{.schema.CodeName}}), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) Get(name string, opts metav1.GetOptions) (*{{.schema.CodeName}}, error) {
|
||||||
|
obj, err := s.objectClient.Get(name, opts)
|
||||||
|
return obj.(*{{.schema.CodeName}}), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) Update(o *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error) {
|
||||||
|
obj, err := s.objectClient.Update(o.Name, o)
|
||||||
|
return obj.(*{{.schema.CodeName}}), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) Delete(name string, options *metav1.DeleteOptions) error {
|
||||||
|
return s.objectClient.Delete(name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) List(opts metav1.ListOptions) (*{{.schema.CodeName}}List, error) {
|
||||||
|
obj, err := s.objectClient.List(opts)
|
||||||
|
return obj.(*{{.schema.CodeName}}List), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) Watch(opts metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
return s.objectClient.Watch(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{.schema.ID}}Client) DeleteCollection(deleteOpts *metav1.DeleteOptions, listOpts metav1.ListOptions) error {
|
||||||
|
return s.objectClient.DeleteCollection(deleteOpts, listOpts)
|
||||||
|
}
|
||||||
|
`
|
37
generator/funcs.go
Normal file
37
generator/funcs.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func funcs() template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
"toLowerCamelCase": convert.LowerTitle,
|
||||||
|
"capitalize": convert.Capitalize,
|
||||||
|
"upper": strings.ToUpper,
|
||||||
|
"toLower": strings.ToLower,
|
||||||
|
"hasGet": hasGet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUnderscore(input string) string {
|
||||||
|
return strings.ToLower(underscoreRegexp.ReplaceAllString(input, `${1}_${2}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasGet(schema *types.Schema) bool {
|
||||||
|
return contains(schema.CollectionMethods, http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(list []string, needle string) bool {
|
||||||
|
for _, i := range list {
|
||||||
|
if i == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
261
generator/generator.go
Normal file
261
generator/generator.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/gengo/args"
|
||||||
|
"k8s.io/gengo/examples/deepcopy-gen/generators"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
blackListTypes = map[string]bool{
|
||||||
|
"schema": true,
|
||||||
|
"resource": true,
|
||||||
|
"collection": true,
|
||||||
|
}
|
||||||
|
underscoreRegexp = regexp.MustCompile(`([a-z])([A-Z])`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func getGoType(field types.Field, schema *types.Schema, schemas *types.Schemas) string {
|
||||||
|
return getTypeString(field.Nullable, field.Type, schema, schemas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTypeString(nullable bool, typeName string, schema *types.Schema, schemas *types.Schemas) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(typeName, "reference["):
|
||||||
|
return "string"
|
||||||
|
case strings.HasPrefix(typeName, "map["):
|
||||||
|
return "map[string]" + getTypeString(false, typeName[len("map["):len(typeName)-1], schema, schemas)
|
||||||
|
case strings.HasPrefix(typeName, "array["):
|
||||||
|
return "[]" + getTypeString(false, typeName[len("array["):len(typeName)-1], schema, schemas)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := ""
|
||||||
|
|
||||||
|
switch typeName {
|
||||||
|
case "json":
|
||||||
|
return "interface{}"
|
||||||
|
case "boolean":
|
||||||
|
name = "bool"
|
||||||
|
case "float":
|
||||||
|
name = "float64"
|
||||||
|
case "int":
|
||||||
|
name = "int64"
|
||||||
|
case "password":
|
||||||
|
return "string"
|
||||||
|
case "date":
|
||||||
|
return "string"
|
||||||
|
case "string":
|
||||||
|
return "string"
|
||||||
|
case "enum":
|
||||||
|
return "string"
|
||||||
|
default:
|
||||||
|
if schema != nil && schemas != nil {
|
||||||
|
otherSchema := schemas.Schema(&schema.Version, typeName)
|
||||||
|
if otherSchema != nil {
|
||||||
|
name = otherSchema.CodeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = convert.Capitalize(typeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nullable {
|
||||||
|
return "*" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTypeMap(schema *types.Schema, schemas *types.Schemas) map[string]string {
|
||||||
|
result := map[string]string{}
|
||||||
|
for _, field := range schema.ResourceFields {
|
||||||
|
result[field.CodeName] = getGoType(field, schema, schemas)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourceActions(schema *types.Schema, schemas *types.Schemas) map[string]types.Action {
|
||||||
|
result := map[string]types.Action{}
|
||||||
|
for name, action := range schema.ResourceActions {
|
||||||
|
if schemas.Schema(&schema.Version, action.Output) != nil {
|
||||||
|
result[name] = action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateType(outputDir string, schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
filePath := strings.ToLower("zz_generated_" + addUnderscore(schema.ID) + ".go")
|
||||||
|
output, err := os.Create(path.Join(outputDir, filePath))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
|
||||||
|
typeTemplate, err := template.New("type.template").
|
||||||
|
Funcs(funcs()).
|
||||||
|
Parse(strings.Replace(typeTemplate, "%BACK%", "`", -1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeTemplate.Execute(output, map[string]interface{}{
|
||||||
|
"schema": schema,
|
||||||
|
"structFields": getTypeMap(schema, schemas),
|
||||||
|
"resourceActions": getResourceActions(schema, schemas),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateController(outputDir string, schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
filePath := strings.ToLower("zz_generated_" + addUnderscore(schema.ID) + "_controller.go")
|
||||||
|
output, err := os.Create(path.Join(outputDir, filePath))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
|
||||||
|
typeTemplate, err := template.New("controller.template").
|
||||||
|
Funcs(funcs()).
|
||||||
|
Parse(strings.Replace(controllerTemplate, "%BACK%", "`", -1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.InternalSchema != nil {
|
||||||
|
schema = schema.InternalSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeTemplate.Execute(output, map[string]interface{}{
|
||||||
|
"schema": schema,
|
||||||
|
"structFields": getTypeMap(schema, schemas),
|
||||||
|
"resourceActions": getResourceActions(schema, schemas),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateClient(outputDir string, schemas []*types.Schema) error {
|
||||||
|
template, err := template.New("client.template").
|
||||||
|
Funcs(funcs()).
|
||||||
|
Parse(clientTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.Create(path.Join(outputDir, "zz_generated_client.go"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
|
||||||
|
return template.Execute(output, map[string]interface{}{
|
||||||
|
"schemas": schemas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Generate(schemas *types.Schemas, cattleOutputPackage, k8sOutputPackage string) error {
|
||||||
|
baseDir := args.DefaultSourceTree()
|
||||||
|
cattleDir := path.Join(baseDir, cattleOutputPackage)
|
||||||
|
k8sDir := path.Join(baseDir, k8sOutputPackage)
|
||||||
|
|
||||||
|
if err := prepareDirs(cattleDir, k8sDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
generated := []*types.Schema{}
|
||||||
|
for _, schema := range schemas.Schemas() {
|
||||||
|
if blackListTypes[schema.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generateType(cattleDir, schema, schemas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if contains(schema.CollectionMethods, http.MethodGet) {
|
||||||
|
if err := generateController(k8sDir, schema, schemas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generated = append(generated, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generateClient(cattleDir, generated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deepCopyGen(baseDir, k8sOutputPackage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gofmt(baseDir, k8sOutputPackage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gofmt(baseDir, cattleOutputPackage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareDirs(dirs ...string) error {
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasPrefix(file.Name(), "zz_generated") {
|
||||||
|
fmt.Println("DELETING", path.Join(dir, file.Name()))
|
||||||
|
//if err != os.Remove(path.Join(dir, file.Name()); err != nil {
|
||||||
|
// return errors.Wrapf(err, "failed to delete %s", path.Join(dir, file.Name()))
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gofmt(workDir, pkg string) error {
|
||||||
|
cmd := exec.Command("go", "fmt", "./"+pkg+"/...")
|
||||||
|
cmd.Dir = workDir
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepCopyGen(workDir, pkg string) error {
|
||||||
|
arguments := &args.GeneratorArgs{
|
||||||
|
InputDirs: []string{pkg},
|
||||||
|
OutputBase: workDir,
|
||||||
|
OutputPackagePath: pkg,
|
||||||
|
OutputFileBaseName: "zz_generated_deepcopy",
|
||||||
|
GoHeaderFilePath: "/dev/null",
|
||||||
|
GeneratedBuildTag: "ignore_autogenerated",
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments.Execute(
|
||||||
|
generators.NameSystems(),
|
||||||
|
generators.DefaultNameSystem(),
|
||||||
|
generators.Packages)
|
||||||
|
}
|
111
generator/type_template.go
Normal file
111
generator/type_template.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
var typeTemplate = `package client
|
||||||
|
|
||||||
|
{{- if .schema | hasGet }}
|
||||||
|
import (
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
const (
|
||||||
|
{{.schema.CodeName}}Type = "{{.schema.ID}}"
|
||||||
|
{{- range $key, $value := .structFields}}
|
||||||
|
{{$.schema.CodeName}}Field{{$key}} = "{{$key | toLowerCamelCase }}"
|
||||||
|
{{- end}}
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{.schema.CodeName}} struct {
|
||||||
|
{{- if .schema | hasGet }}
|
||||||
|
types.Resource
|
||||||
|
{{- end}}
|
||||||
|
{{- range $key, $value := .structFields}}
|
||||||
|
{{$key}} {{$value}} %BACK%json:"{{$key | toLowerCamelCase }},omitempty"%BACK%
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if .schema | hasGet }}
|
||||||
|
type {{.schema.CodeName}}Collection struct {
|
||||||
|
types.Collection
|
||||||
|
Data []{{.schema.CodeName}} %BACK%json:"data,omitempty"%BACK%
|
||||||
|
client *{{.schema.CodeName}}Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{.schema.CodeName}}Client struct {
|
||||||
|
apiClient *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{.schema.CodeName}}Operations interface {
|
||||||
|
List(opts *types.ListOpts) (*{{.schema.CodeName}}Collection, error)
|
||||||
|
Create(opts *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error)
|
||||||
|
Update(existing *{{.schema.CodeName}}, updates interface{}) (*{{.schema.CodeName}}, error)
|
||||||
|
ById(id string) (*{{.schema.CodeName}}, error)
|
||||||
|
Delete(container *{{.schema.CodeName}}) error{{range $key, $value := .resourceActions}}
|
||||||
|
{{if eq $value.Input "" }}
|
||||||
|
Action{{$key | capitalize}} (*{{$.schema.CodeName}}) (*{{.Output | capitalize}}, error)
|
||||||
|
{{else}}
|
||||||
|
Action{{$key | capitalize}} (*{{$.schema.CodeName}}, *{{$value.Input | capitalize}}) (*{{.Output | capitalize}}, error)
|
||||||
|
{{end}}{{end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func new{{.schema.CodeName}}Client(apiClient *Client) *{{.schema.CodeName}}Client {
|
||||||
|
return &{{.schema.CodeName}}Client{
|
||||||
|
apiClient: apiClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *{{.schema.CodeName}}Client) Create(container *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error) {
|
||||||
|
resp := &{{.schema.CodeName}}{}
|
||||||
|
err := c.apiClient.Ops.DoCreate({{.schema.CodeName}}Type, container, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *{{.schema.CodeName}}Client) Update(existing *{{.schema.CodeName}}, updates interface{}) (*{{.schema.CodeName}}, error) {
|
||||||
|
resp := &{{.schema.CodeName}}{}
|
||||||
|
err := c.apiClient.Ops.DoUpdate({{.schema.CodeName}}Type, &existing.Resource, updates, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *{{.schema.CodeName}}Client) List(opts *types.ListOpts) (*{{.schema.CodeName}}Collection, error) {
|
||||||
|
resp := &{{.schema.CodeName}}Collection{}
|
||||||
|
err := c.apiClient.Ops.DoList({{.schema.CodeName}}Type, opts, resp)
|
||||||
|
resp.client = c
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *{{.schema.CodeName}}Collection) Next() (*{{.schema.CodeName}}Collection, error) {
|
||||||
|
if cc != nil && cc.Pagination != nil && cc.Pagination.Next != "" {
|
||||||
|
resp := &{{.schema.CodeName}}Collection{}
|
||||||
|
err := cc.client.apiClient.Ops.DoNext(cc.Pagination.Next, resp)
|
||||||
|
resp.client = cc.client
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *{{.schema.CodeName}}Client) ById(id string) (*{{.schema.CodeName}}, error) {
|
||||||
|
resp := &{{.schema.CodeName}}{}
|
||||||
|
err := c.apiClient.Ops.DoById({{.schema.CodeName}}Type, id, resp)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *{{.schema.CodeName}}Client) Delete(container *{{.schema.CodeName}}) error {
|
||||||
|
return c.apiClient.Ops.DoResourceDelete({{.schema.CodeName}}Type, &container.Resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
{{range $key, $value := .resourceActions}}
|
||||||
|
{{if eq $value.Input "" }}
|
||||||
|
func (c *{{$.schema.CodeName}}Client) Action{{$key | capitalize}} (resource *{{$.schema.CodeName}}) (*{{.Output | capitalize}}, error) {
|
||||||
|
{{else}}
|
||||||
|
func (c *{{$.schema.CodeName}}Client) Action{{$key | capitalize}} (resource *{{$.schema.CodeName}}, input *{{$value.Input | capitalize}}) (*{{.Output | capitalize}}, error) {
|
||||||
|
{{end}}
|
||||||
|
resp := &{{.Output | capitalize}}{}
|
||||||
|
{{if eq $value.Input "" }}
|
||||||
|
err := c.apiClient.Ops.DoAction({{$.schema.CodeName}}Type, "{{$key}}", &resource.Resource, nil, resp)
|
||||||
|
{{else}}
|
||||||
|
err := c.apiClient.Ops.DoAction({{$.schema.CodeName}}Type, "{{$key}}", &resource.Resource, input, resp)
|
||||||
|
{{end}}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
{{end}}`
|
@@ -1,130 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rancher/go-rancher/v3"
|
|
||||||
"github.com/rancher/norman/query"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ASC = SortOrder("asc")
|
|
||||||
DESC = SortOrder("desc")
|
|
||||||
defaultLimit = 100
|
|
||||||
maxLimit = 3000
|
|
||||||
)
|
|
||||||
|
|
||||||
type SortOrder string
|
|
||||||
|
|
||||||
type Pagination struct {
|
|
||||||
Limit int
|
|
||||||
Marker string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CollectionAttributes struct {
|
|
||||||
Sort string
|
|
||||||
Order SortOrder
|
|
||||||
Pagination *Pagination
|
|
||||||
Conditions []*query.Condition
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseCollectionAttributes(req *http.Request, schema client.Schema) *CollectionAttributes {
|
|
||||||
if req.Method != http.MethodGet {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &CollectionAttributes{}
|
|
||||||
|
|
||||||
result.Order = parseOrder(req)
|
|
||||||
result.Sort = parseSort(schema, req)
|
|
||||||
result.Pagination = parsePagination(req)
|
|
||||||
result.Conditions = parseFilters(schema, req)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOrder(req *http.Request) SortOrder {
|
|
||||||
order := req.URL.Query().Get("order")
|
|
||||||
if SortOrder(order) == DESC {
|
|
||||||
return DESC
|
|
||||||
}
|
|
||||||
return ASC
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSort(schema client.Schema, req *http.Request) string {
|
|
||||||
sort := req.URL.Query().Get("sort")
|
|
||||||
if _, ok := schema.CollectionFilters[sort]; ok {
|
|
||||||
return sort
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePagination(req *http.Request) *Pagination {
|
|
||||||
q := req.URL.Query()
|
|
||||||
limit := q.Get("limit")
|
|
||||||
marker := q.Get("marker")
|
|
||||||
|
|
||||||
result := &Pagination{
|
|
||||||
Limit: defaultLimit,
|
|
||||||
Marker: marker,
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit != "" {
|
|
||||||
limitInt, err := strconv.Atoi(limit)
|
|
||||||
if err != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
if limitInt > maxLimit {
|
|
||||||
result.Limit = maxLimit
|
|
||||||
} else if limitInt > 0 {
|
|
||||||
result.Limit = limitInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNameAndOp(value string) (string, string) {
|
|
||||||
name := value
|
|
||||||
op := "eq"
|
|
||||||
|
|
||||||
idx := strings.LastIndex(value, "_")
|
|
||||||
if idx > 0 {
|
|
||||||
op = value[idx+1:]
|
|
||||||
name = value[0:idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
return name, op
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFilters(schema client.Schema, req *http.Request) []*query.Condition {
|
|
||||||
conditions := []*query.Condition{}
|
|
||||||
q := req.URL.Query()
|
|
||||||
for key, values := range req.URL.Query() {
|
|
||||||
name, op := parseNameAndOp(key)
|
|
||||||
filter, ok := schema.CollectionFilters[name]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mod := range filter.Modifiers {
|
|
||||||
if op != mod || !query.ValidMod(op) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
genericValues := []interface{}{}
|
|
||||||
for _, value := range values {
|
|
||||||
genericValues = append(genericValues, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
conditions = append(conditions, query.NewCondition(query.ConditionType(mod), genericValues))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conditions
|
|
||||||
}
|
|
@@ -5,28 +5,38 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
INVALID_DATE_FORMAT = ErrorCode("InvalidDateFormat")
|
INVALID_DATE_FORMAT = ErrorCode{"InvalidDateFormat", 422}
|
||||||
INVALID_FORMAT = ErrorCode("InvalidFormat")
|
INVALID_FORMAT = ErrorCode{"InvalidFormat", 422}
|
||||||
INVALID_REFERENCE = ErrorCode("InvalidReference")
|
INVALID_REFERENCE = ErrorCode{"InvalidReference", 422}
|
||||||
NOT_NULLABLE = ErrorCode("NotNullable")
|
NOT_NULLABLE = ErrorCode{"NotNullable", 422}
|
||||||
NOT_UNIQUE = ErrorCode("NotUnique")
|
NOT_UNIQUE = ErrorCode{"NotUnique", 422}
|
||||||
MIN_LIMIT_EXCEEDED = ErrorCode("MinLimitExceeded")
|
MIN_LIMIT_EXCEEDED = ErrorCode{"MinLimitExceeded", 422}
|
||||||
MAX_LIMIT_EXCEEDED = ErrorCode("MaxLimitExceeded")
|
MAX_LIMIT_EXCEEDED = ErrorCode{"MaxLimitExceeded", 422}
|
||||||
MIN_LENGTH_EXCEEDED = ErrorCode("MinLengthExceeded")
|
MIN_LENGTH_EXCEEDED = ErrorCode{"MinLengthExceeded", 422}
|
||||||
MAX_LENGTH_EXCEEDED = ErrorCode("MaxLengthExceeded")
|
MAX_LENGTH_EXCEEDED = ErrorCode{"MaxLengthExceeded", 422}
|
||||||
INVALID_OPTION = ErrorCode("InvalidOption")
|
INVALID_OPTION = ErrorCode{"InvalidOption", 422}
|
||||||
INVALID_CHARACTERS = ErrorCode("InvalidCharacters")
|
INVALID_CHARACTERS = ErrorCode{"InvalidCharacters", 422}
|
||||||
MISSING_REQUIRED = ErrorCode("MissingRequired")
|
MISSING_REQUIRED = ErrorCode{"MissingRequired", 422}
|
||||||
INVALID_CSRF_TOKEN = ErrorCode("InvalidCSRFToken")
|
INVALID_CSRF_TOKEN = ErrorCode{"InvalidCSRFToken", 422}
|
||||||
INVALID_ACTION = ErrorCode("InvalidAction")
|
INVALID_ACTION = ErrorCode{"InvalidAction", 422}
|
||||||
INVALID_BODY_CONTENT = ErrorCode("InvalidBodyContent")
|
INVALID_BODY_CONTENT = ErrorCode{"InvalidBodyContent", 422}
|
||||||
INVALID_TYPE = ErrorCode("InvalidType")
|
INVALID_TYPE = ErrorCode{"InvalidType", 422}
|
||||||
ACTION_NOT_AVAILABLE = ErrorCode("ActionNotAvailable")
|
ACTION_NOT_AVAILABLE = ErrorCode{"ActionNotAvailable", 404}
|
||||||
INVALID_STATE = ErrorCode("InvalidState")
|
INVALID_STATE = ErrorCode{"InvalidState", 422}
|
||||||
SERVER_ERROR = ErrorCode("ServerError")
|
SERVER_ERROR = ErrorCode{"ServerError", 500}
|
||||||
|
|
||||||
|
METHOD_NOT_ALLOWED = ErrorCode{"MethodNotAllow", 405}
|
||||||
|
NOT_FOUND = ErrorCode{"NotFound", 404}
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrorCode string
|
type ErrorCode struct {
|
||||||
|
code string
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrorCode) String() string {
|
||||||
|
return fmt.Sprintf("%s %d", e.code, e.status)
|
||||||
|
}
|
||||||
|
|
||||||
type APIError struct {
|
type APIError struct {
|
||||||
code ErrorCode
|
code ErrorCode
|
||||||
@@ -52,7 +62,7 @@ func NewFieldAPIError(code ErrorCode, fieldName, message string) error {
|
|||||||
|
|
||||||
func WrapFieldAPIError(err error, code ErrorCode, fieldName, message string) error {
|
func WrapFieldAPIError(err error, code ErrorCode, fieldName, message string) error {
|
||||||
return &APIError{
|
return &APIError{
|
||||||
Cause: err,
|
Cause: err,
|
||||||
code: code,
|
code: code,
|
||||||
message: message,
|
message: message,
|
||||||
fieldName: fieldName,
|
fieldName: fieldName,
|
||||||
@@ -68,5 +78,8 @@ func WrapAPIError(err error, code ErrorCode, message string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *APIError) Error() string {
|
func (a *APIError) Error() string {
|
||||||
|
if a.fieldName != "" {
|
||||||
|
return fmt.Sprintf("%s=%s: %s", a.fieldName, a.code, a.message)
|
||||||
|
}
|
||||||
return fmt.Sprintf("%s: %s", a.code, a.message)
|
return fmt.Sprintf("%s: %s", a.code, a.message)
|
||||||
}
|
}
|
||||||
|
35
httperror/handler.go
Normal file
35
httperror/handler.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package httperror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrorHandler(request *types.APIContext, err error) {
|
||||||
|
var error *APIError
|
||||||
|
if apiError, ok := err.(*APIError); ok {
|
||||||
|
error = apiError
|
||||||
|
} else {
|
||||||
|
logrus.Errorf("Unknown error: %v", err)
|
||||||
|
error = &APIError{
|
||||||
|
code: SERVER_ERROR,
|
||||||
|
message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := toError(error)
|
||||||
|
request.WriteResponse(error.code.status, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(apiError *APIError) map[string]interface{} {
|
||||||
|
e := map[string]interface{}{
|
||||||
|
"type": "/v3/schema",
|
||||||
|
"code": apiError.code.code,
|
||||||
|
"message": apiError.message,
|
||||||
|
}
|
||||||
|
if apiError.fieldName != "" {
|
||||||
|
e["fieldName"] = apiError.fieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
23
main.go
23
main.go
@@ -1,23 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
var VERSION = "v0.0.0-dev"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := cli.NewApp()
|
|
||||||
app.Name = "norman"
|
|
||||||
app.Version = VERSION
|
|
||||||
app.Usage = "You need help!"
|
|
||||||
app.Action = func(c *cli.Context) error {
|
|
||||||
logrus.Info("I'm a turkey")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Run(os.Args)
|
|
||||||
}
|
|
23
name/name.go
Normal file
23
name/name.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package name
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func GuessPluralName(name string) string {
|
||||||
|
if name == "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
if suffix(name, "s") || suffix(name, "ch") || suffix(name, "x") {
|
||||||
|
return name + "es"
|
||||||
|
}
|
||||||
|
|
||||||
|
if suffix(name, "y") && len(name) > 2 && !strings.ContainsAny(name[len(name)-2:len(name)-1], "[aeiou]") {
|
||||||
|
return name[0:len(name)-1] + "ies"
|
||||||
|
}
|
||||||
|
|
||||||
|
return name + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
func suffix(str, end string) bool {
|
||||||
|
return strings.HasSuffix(str, end)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package parse
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,5 +14,5 @@ func IsBrowser(req *http.Request, checkAccepts bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User agent has Mozilla and browser accepts */*
|
// User agent has Mozilla and browser accepts */*
|
||||||
return strings.Contains(userAgent, "mozilla") && strings.Contains(accepts,"*/*")
|
return strings.Contains(userAgent, "mozilla") && strings.Contains(accepts, "*/*")
|
||||||
}
|
}
|
@@ -1,34 +1,43 @@
|
|||||||
package handler
|
package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rancher/go-rancher/v3"
|
|
||||||
"github.com/rancher/norman/httperror"
|
"github.com/rancher/norman/httperror"
|
||||||
"github.com/rancher/norman/registry"
|
"github.com/rancher/norman/types"
|
||||||
"github.com/rancher/norman/store"
|
"github.com/rancher/norman/types/convert"
|
||||||
|
"github.com/rancher/norman/types/definition"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Create = Operation("create")
|
Create = Operation("create")
|
||||||
Update = Operation("update")
|
Update = Operation("update")
|
||||||
Action = Operation("action")
|
Action = Operation("action")
|
||||||
|
List = Operation("list")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Operation string
|
type Operation string
|
||||||
|
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
Registry registry.SchemaRegistry
|
Version *types.APIVersion
|
||||||
RefValidator store.ReferenceValidator
|
Schemas *types.Schemas
|
||||||
|
RefValidator types.ReferenceValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Construct(schema *client.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
|
func NewBuilder(apiRequest *types.APIContext) *Builder {
|
||||||
|
return &Builder{
|
||||||
|
Version: apiRequest.Version,
|
||||||
|
Schemas: apiRequest.Schemas,
|
||||||
|
RefValidator: apiRequest.ReferenceValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Construct(schema *types.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
|
||||||
return b.copyFields(schema, input, op)
|
return b.copyFields(schema, input, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) copyInputs(schema *client.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
|
func (b *Builder) copyInputs(schema *types.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
|
||||||
for fieldName, value := range input {
|
for fieldName, value := range input {
|
||||||
field, ok := schema.ResourceFields[fieldName]
|
field, ok := schema.ResourceFields[fieldName]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -46,28 +55,37 @@ func (b *Builder) copyInputs(schema *client.Schema, input map[string]interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
if value != nil || wasNull {
|
if value != nil || wasNull {
|
||||||
if slice, ok := value.([]interface{}); ok {
|
if op != List {
|
||||||
for _, sliceValue := range slice {
|
if slice, ok := value.([]interface{}); ok {
|
||||||
if sliceValue == nil {
|
for _, sliceValue := range slice {
|
||||||
return httperror.NewFieldAPIError(httperror.NOT_NULLABLE, fieldName, "Individual array values can not be null")
|
if sliceValue == nil {
|
||||||
|
return httperror.NewFieldAPIError(httperror.NOT_NULLABLE, fieldName, "Individual array values can not be null")
|
||||||
|
}
|
||||||
|
if err := checkFieldCriteria(fieldName, field, sliceValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := checkFieldCriteria(fieldName, field, sliceValue); err != nil {
|
} else {
|
||||||
|
if err := checkFieldCriteria(fieldName, field, value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if err := checkFieldCriteria(fieldName, field, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result[fieldName] = value
|
result[fieldName] = value
|
||||||
|
|
||||||
|
if op == List && field.Type == "date" && value != "" {
|
||||||
|
ts, err := convert.ToTimestamp(value)
|
||||||
|
if err == nil {
|
||||||
|
result[fieldName+"TS"] = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) checkDefaultAndRequired(schema *client.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
|
func (b *Builder) checkDefaultAndRequired(schema *types.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
|
||||||
for fieldName, field := range schema.ResourceFields {
|
for fieldName, field := range schema.ResourceFields {
|
||||||
_, hasKey := result[fieldName]
|
_, hasKey := result[fieldName]
|
||||||
if op == Create && !hasKey && field.Default != nil {
|
if op == Create && !hasKey && field.Default != nil {
|
||||||
@@ -80,7 +98,7 @@ func (b *Builder) checkDefaultAndRequired(schema *client.Schema, input map[strin
|
|||||||
return httperror.NewFieldAPIError(httperror.MISSING_REQUIRED, fieldName, "")
|
return httperror.NewFieldAPIError(httperror.MISSING_REQUIRED, fieldName, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isArrayType(field.Type) {
|
if definition.IsArrayType(field.Type) {
|
||||||
slice, err := b.convertArray(fieldName, result[fieldName], op)
|
slice, err := b.convertArray(fieldName, result[fieldName], op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -95,7 +113,7 @@ func (b *Builder) checkDefaultAndRequired(schema *client.Schema, input map[strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) copyFields(schema *client.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
|
func (b *Builder) copyFields(schema *types.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
|
||||||
result := map[string]interface{}{}
|
result := map[string]interface{}{}
|
||||||
|
|
||||||
if err := b.copyInputs(schema, input, op, result); err != nil {
|
if err := b.copyInputs(schema, input, op, result); err != nil {
|
||||||
@@ -105,7 +123,7 @@ func (b *Builder) copyFields(schema *client.Schema, input map[string]interface{}
|
|||||||
return result, b.checkDefaultAndRequired(schema, input, op, result)
|
return result, b.checkDefaultAndRequired(schema, input, op, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkFieldCriteria(fieldName string, field client.Field, value interface{}) error {
|
func checkFieldCriteria(fieldName string, field types.Field, value interface{}) error {
|
||||||
numVal, isNum := value.(int64)
|
numVal, isNum := value.(int64)
|
||||||
strVal := ""
|
strVal := ""
|
||||||
hasStrVal := false
|
hasStrVal := false
|
||||||
@@ -133,10 +151,10 @@ func checkFieldCriteria(fieldName string, field client.Field, value interface{})
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasStrVal {
|
if hasStrVal {
|
||||||
if field.MinLength != nil && len(strVal) < *field.MinLength {
|
if field.MinLength != nil && int64(len(strVal)) < *field.MinLength {
|
||||||
return httperror.NewFieldAPIError(httperror.MIN_LENGTH_EXCEEDED, fieldName, "")
|
return httperror.NewFieldAPIError(httperror.MIN_LENGTH_EXCEEDED, fieldName, "")
|
||||||
}
|
}
|
||||||
if field.MaxLength != nil && len(strVal) > *field.MaxLength {
|
if field.MaxLength != nil && int64(len(strVal)) > *field.MaxLength {
|
||||||
return httperror.NewFieldAPIError(httperror.MAX_LENGTH_EXCEEDED, fieldName, "")
|
return httperror.NewFieldAPIError(httperror.MAX_LENGTH_EXCEEDED, fieldName, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,11 +199,11 @@ func (b *Builder) convert(fieldType string, value interface{}, op Operation) (in
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isMapType(fieldType):
|
case definition.IsMapType(fieldType):
|
||||||
return b.convertMap(fieldType, value, op), nil
|
return b.convertMap(fieldType, value, op)
|
||||||
case isArrayType(fieldType):
|
case definition.IsArrayType(fieldType):
|
||||||
return b.convertArray(fieldType, value, op), nil
|
return b.convertArray(fieldType, value, op)
|
||||||
case isReferenceType(fieldType):
|
case definition.IsReferenceType(fieldType):
|
||||||
return b.convertReferenceType(fieldType, value)
|
return b.convertReferenceType(fieldType, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,26 +211,26 @@ func (b *Builder) convert(fieldType string, value interface{}, op Operation) (in
|
|||||||
case "json":
|
case "json":
|
||||||
return value, nil
|
return value, nil
|
||||||
case "date":
|
case "date":
|
||||||
return convertString(value), nil
|
return convert.ToString(value), nil
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return convertBool(value), nil
|
return convert.ToBool(value), nil
|
||||||
case "enum":
|
case "enum":
|
||||||
return convertString(value), nil
|
return convert.ToString(value), nil
|
||||||
case "int":
|
case "int":
|
||||||
return convertNumber(value)
|
return convert.ToNumber(value)
|
||||||
case "password":
|
case "password":
|
||||||
return convertString(value), nil
|
return convert.ToString(value), nil
|
||||||
case "string":
|
case "string":
|
||||||
return convertString(value), nil
|
return convert.ToString(value), nil
|
||||||
case "reference":
|
case "reference":
|
||||||
return convertString(value), nil
|
return convert.ToString(value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.convertType(fieldType, value, op)
|
return b.convertType(fieldType, value, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) convertType(fieldType string, value interface{}, op Operation) (interface{}, error) {
|
func (b *Builder) convertType(fieldType string, value interface{}, op Operation) (interface{}, error) {
|
||||||
schema := b.Registry.GetSchema(fieldType)
|
schema := b.Schemas.Schema(b.Version, fieldType)
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return nil, httperror.NewAPIError(httperror.INVALID_TYPE, "Failed to find type "+fieldType)
|
return nil, httperror.NewAPIError(httperror.INVALID_TYPE, "Failed to find type "+fieldType)
|
||||||
}
|
}
|
||||||
@@ -225,45 +243,32 @@ func (b *Builder) convertType(fieldType string, value interface{}, op Operation)
|
|||||||
return b.Construct(schema, mapValue, op)
|
return b.Construct(schema, mapValue, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertNumber(value interface{}) (int64, error) {
|
|
||||||
i, ok := value.(int64)
|
|
||||||
if ok {
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
return strconv.ParseInt(convertString(value), 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertBool(value interface{}) bool {
|
|
||||||
b, ok := value.(bool)
|
|
||||||
if ok {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
str := strings.ToLower(convertString(value))
|
|
||||||
return str == "true" || str == "t" || str == "yes" || str == "y"
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertString(value interface{}) string {
|
|
||||||
return fmt.Sprint(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) convertReferenceType(fieldType string, value interface{}) (string, error) {
|
func (b *Builder) convertReferenceType(fieldType string, value interface{}) (string, error) {
|
||||||
subType := fieldType[len("array[") : len(fieldType)-1]
|
subType := definition.SubType(fieldType)
|
||||||
strVal := convertString(value)
|
strVal := convert.ToString(value)
|
||||||
if !b.RefValidator.Validate(subType, strVal) {
|
if b.RefValidator != nil && !b.RefValidator.Validate(subType, strVal) {
|
||||||
return "", httperror.NewAPIError(httperror.INVALID_REFERENCE, fmt.Sprintf("Not found type: %s id: %s", subType, strVal))
|
return "", httperror.NewAPIError(httperror.INVALID_REFERENCE, fmt.Sprintf("Not found type: %s id: %s", subType, strVal))
|
||||||
}
|
}
|
||||||
return strVal, nil
|
return strVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) convertArray(fieldType string, value interface{}, op Operation) ([]interface{}, error) {
|
func (b *Builder) convertArray(fieldType string, value interface{}, op Operation) ([]interface{}, error) {
|
||||||
|
if strSliceValue, ok := value.([]string); ok {
|
||||||
|
// Form data will be []string
|
||||||
|
result := []interface{}{}
|
||||||
|
for _, value := range strSliceValue {
|
||||||
|
result = append(result, value)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
sliceValue, ok := value.([]interface{})
|
sliceValue, ok := value.([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := []interface{}{}
|
result := []interface{}{}
|
||||||
subType := fieldType[len("array[") : len(fieldType)-1]
|
subType := definition.SubType(fieldType)
|
||||||
|
|
||||||
for _, value := range sliceValue {
|
for _, value := range sliceValue {
|
||||||
val, err := b.convert(subType, value, op)
|
val, err := b.convert(subType, value, op)
|
||||||
@@ -283,7 +288,7 @@ func (b *Builder) convertMap(fieldType string, value interface{}, op Operation)
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]interface{}{}
|
result := map[string]interface{}{}
|
||||||
subType := fieldType[len("map[") : len(fieldType)-1]
|
subType := definition.SubType(fieldType)
|
||||||
|
|
||||||
for key, value := range mapValue {
|
for key, value := range mapValue {
|
||||||
val, err := b.convert(subType, value, op)
|
val, err := b.convert(subType, value, op)
|
||||||
@@ -296,24 +301,14 @@ func (b *Builder) convertMap(fieldType string, value interface{}, op Operation)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMapType(fieldType string) bool {
|
func fieldMatchesOp(field types.Field, op Operation) bool {
|
||||||
return strings.HasPrefix(fieldType, "map[") && strings.HasSuffix(fieldType, "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isArrayType(fieldType string) bool {
|
|
||||||
return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isReferenceType(fieldType string) bool {
|
|
||||||
return strings.HasPrefix(fieldType, "reference[") && strings.HasSuffix(fieldType, "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func fieldMatchesOp(field client.Field, op Operation) bool {
|
|
||||||
switch op {
|
switch op {
|
||||||
case Create:
|
case Create:
|
||||||
return field.Create
|
return field.Create
|
||||||
case Update:
|
case Update:
|
||||||
return field.Update
|
return field.Update
|
||||||
|
case List:
|
||||||
|
return !field.WriteOnly
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
228
parse/parse.go
Normal file
228
parse/parse.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/api/builtin"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/urlbuilder"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxFormSize = 2 * 1 << 20
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
multiSlashRegexp = regexp.MustCompile("//+")
|
||||||
|
allowedFormats = map[string]bool{
|
||||||
|
"html": true,
|
||||||
|
"json": true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas) (*types.APIContext, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
result := &types.APIContext{
|
||||||
|
Request: req,
|
||||||
|
Response: rw,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response format is guarenteed to be set even in the event of an error
|
||||||
|
result.ResponseFormat = parseResponseFormat(req)
|
||||||
|
result.Version = parseVersion(schemas, req.URL.Path)
|
||||||
|
result.Schemas = schemas
|
||||||
|
|
||||||
|
if result.Version == nil {
|
||||||
|
result.Method = http.MethodGet
|
||||||
|
result.URLBuilder, err = urlbuilder.New(req, types.APIVersion{}, result.Schemas)
|
||||||
|
result.Type = "apiRoot"
|
||||||
|
result.Schema = &builtin.APIRoot
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Method = parseMethod(req)
|
||||||
|
result.Action, result.Method = parseAction(req, result.Method)
|
||||||
|
result.Body, err = parseBody(req)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.URLBuilder, err = urlbuilder.New(req, *result.Version, result.Schemas)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePath(result, req)
|
||||||
|
|
||||||
|
if result.Schema == nil {
|
||||||
|
result.Method = http.MethodGet
|
||||||
|
result.Type = "apiRoot"
|
||||||
|
result.Schema = &builtin.APIRoot
|
||||||
|
result.ID = result.Version.Path
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateMethod(result); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSubContext(parts []string, apiRequest *types.APIContext) []string {
|
||||||
|
subContext := ""
|
||||||
|
apiRequest.SubContext = map[string]interface{}{}
|
||||||
|
|
||||||
|
for len(parts) > 3 && apiRequest.Version != nil {
|
||||||
|
resourceType := parts[1]
|
||||||
|
resourceID := parts[2]
|
||||||
|
|
||||||
|
if !apiRequest.Version.SubContexts[resourceType] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiRequest.ReferenceValidator != nil && !apiRequest.ReferenceValidator.Validate(resourceType, resourceID) {
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRequest.SubContext[resourceType] = resourceID
|
||||||
|
subContext = subContext + "/" + resourceType + "/" + resourceID
|
||||||
|
parts = append(parts[:1], parts[3:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if subContext != "" {
|
||||||
|
apiRequest.URLBuilder.SetSubContext(subContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePath(apiRequest *types.APIContext, request *http.Request) {
|
||||||
|
if apiRequest.Version == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := request.URL.Path
|
||||||
|
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
||||||
|
|
||||||
|
versionPrefix := apiRequest.Version.Path
|
||||||
|
if !strings.HasPrefix(path, versionPrefix) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(path[len(versionPrefix):], "/")
|
||||||
|
parts = parseSubContext(parts, apiRequest)
|
||||||
|
|
||||||
|
if len(parts) > 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
typeName := safeIndex(parts, 1)
|
||||||
|
id := safeIndex(parts, 2)
|
||||||
|
link := safeIndex(parts, 3)
|
||||||
|
|
||||||
|
if typeName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := apiRequest.Schemas.Schema(apiRequest.Version, typeName)
|
||||||
|
if schema == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRequest.Schema = schema
|
||||||
|
apiRequest.Type = schema.ID
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRequest.ID = id
|
||||||
|
apiRequest.Link = link
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeIndex(slice []string, index int) string {
|
||||||
|
if index >= len(slice) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return slice[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponseFormat(req *http.Request) string {
|
||||||
|
format := req.URL.Query().Get("_format")
|
||||||
|
|
||||||
|
if format != "" {
|
||||||
|
format = strings.TrimSpace(strings.ToLower(format))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Format specified */
|
||||||
|
if allowedFormats[format] {
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// User agent has Mozilla and browser accepts */*
|
||||||
|
if IsBrowser(req, true) {
|
||||||
|
return "html"
|
||||||
|
} else {
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMethod(req *http.Request) string {
|
||||||
|
method := req.URL.Query().Get("_method")
|
||||||
|
if method == "" {
|
||||||
|
method = req.Method
|
||||||
|
}
|
||||||
|
return method
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAction(req *http.Request, method string) (string, string) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
return "", method
|
||||||
|
}
|
||||||
|
|
||||||
|
action := req.URL.Query().Get("action")
|
||||||
|
if action == "remove" {
|
||||||
|
return "", http.MethodDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
return action, method
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVersion(schemas *types.Schemas, path string) *types.APIVersion {
|
||||||
|
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
||||||
|
for _, version := range schemas.Versions() {
|
||||||
|
if version.Path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, version.Path) {
|
||||||
|
return &version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBody(req *http.Request) (map[string]interface{}, error) {
|
||||||
|
req.ParseMultipartForm(maxFormSize)
|
||||||
|
if req.MultipartForm != nil {
|
||||||
|
return valuesToBody(req.MultipartForm.Value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Form != nil && len(req.Form) > 0 {
|
||||||
|
return valuesToBody(map[string][]string(req.Form)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadBody(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valuesToBody(input map[string][]string) map[string]interface{} {
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
for k, v := range input {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
112
parse/parse_collection.go
Normal file
112
parse/parse_collection.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultLimit = int64(100)
|
||||||
|
maxLimit = int64(3000)
|
||||||
|
)
|
||||||
|
|
||||||
|
func QueryOptions(req *http.Request, schema *types.Schema) *types.QueryOptions {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &types.QueryOptions{}
|
||||||
|
|
||||||
|
result.Sort = parseSort(schema, req)
|
||||||
|
result.Pagination = parsePagination(req)
|
||||||
|
result.Conditions = parseFilters(schema, req)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOrder(req *http.Request) types.SortOrder {
|
||||||
|
order := req.URL.Query().Get("order")
|
||||||
|
if types.SortOrder(order) == types.DESC {
|
||||||
|
return types.DESC
|
||||||
|
}
|
||||||
|
return types.ASC
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSort(schema *types.Schema, req *http.Request) types.Sort {
|
||||||
|
sortField := req.URL.Query().Get("sort")
|
||||||
|
if _, ok := schema.CollectionFilters[sortField]; !ok {
|
||||||
|
sortField = ""
|
||||||
|
}
|
||||||
|
return types.Sort{
|
||||||
|
Order: parseOrder(req),
|
||||||
|
Name: sortField,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePagination(req *http.Request) *types.Pagination {
|
||||||
|
q := req.URL.Query()
|
||||||
|
limit := q.Get("limit")
|
||||||
|
marker := q.Get("marker")
|
||||||
|
|
||||||
|
result := &types.Pagination{
|
||||||
|
Limit: &defaultLimit,
|
||||||
|
Marker: marker,
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit != "" {
|
||||||
|
limitInt, err := strconv.ParseInt(limit, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitInt > maxLimit {
|
||||||
|
result.Limit = &maxLimit
|
||||||
|
} else if limitInt > 0 {
|
||||||
|
result.Limit = &limitInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNameAndOp(value string) (string, string) {
|
||||||
|
name := value
|
||||||
|
op := "eq"
|
||||||
|
|
||||||
|
idx := strings.LastIndex(value, "_")
|
||||||
|
if idx > 0 {
|
||||||
|
op = value[idx+1:]
|
||||||
|
name = value[0:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, op
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFilters(schema *types.Schema, req *http.Request) []*types.QueryCondition {
|
||||||
|
conditions := []*types.QueryCondition{}
|
||||||
|
for key, values := range req.URL.Query() {
|
||||||
|
name, op := parseNameAndOp(key)
|
||||||
|
filter, ok := schema.CollectionFilters[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mod := range filter.Modifiers {
|
||||||
|
if op != mod || !types.ValidMod(op) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
genericValues := []interface{}{}
|
||||||
|
for _, value := range values {
|
||||||
|
genericValues = append(genericValues, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, types.NewConditionFromString(name, mod, genericValues))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
}
|
@@ -1,22 +1,23 @@
|
|||||||
package handler
|
package parse
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"io/ioutil"
|
|
||||||
"io"
|
|
||||||
"github.com/rancher/norman/httperror"
|
|
||||||
"fmt"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/httperror"
|
||||||
)
|
)
|
||||||
|
|
||||||
const reqMaxSize = (2 * 1<<20) + 1
|
const reqMaxSize = (2 * 1 << 20) + 1
|
||||||
|
|
||||||
var bodyMethods = map[string]bool{
|
var bodyMethods = map[string]bool{
|
||||||
http.MethodPut: true,
|
http.MethodPut: true,
|
||||||
http.MethodPost: true,
|
http.MethodPost: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadBody(rw http.ResponseWriter, req *http.Request) (map[string]interface{}, error) {
|
func ReadBody(req *http.Request) (map[string]interface{}, error) {
|
||||||
if !bodyMethods[req.Method] {
|
if !bodyMethods[req.Method] {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ func ReadBody(rw http.ResponseWriter, req *http.Request) (map[string]interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := map[string]interface{}{}
|
data := map[string]interface{}{}
|
||||||
if err := json.Unmarshal(content, data); err != nil {
|
if err := json.Unmarshal(content, &data); err != nil {
|
||||||
return nil, httperror.NewAPIError(httperror.INVALID_BODY_CONTENT,
|
return nil, httperror.NewAPIError(httperror.INVALID_BODY_CONTENT,
|
||||||
fmt.Sprintf("Failed to parse body: %v", err))
|
fmt.Sprintf("Failed to parse body: %v", err))
|
||||||
}
|
}
|
45
parse/validate.go
Normal file
45
parse/validate.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/httperror"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
supportedMethods = map[string]bool{
|
||||||
|
http.MethodPost: true,
|
||||||
|
http.MethodGet: true,
|
||||||
|
http.MethodPut: true,
|
||||||
|
http.MethodDelete: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateMethod(request *types.APIContext) error {
|
||||||
|
if request.Action != "" && request.Method == http.MethodPost {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !supportedMethods[request.Method] {
|
||||||
|
return httperror.NewAPIError(httperror.METHOD_NOT_ALLOWED, fmt.Sprintf("Method %s not supported", request.Method))
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Type == "" || request.Schema == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed := request.Schema.ResourceMethods
|
||||||
|
if request.ID == "" {
|
||||||
|
allowed = request.Schema.CollectionMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, method := range allowed {
|
||||||
|
if method == request.Method {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return httperror.NewAPIError(httperror.METHOD_NOT_ALLOWED, fmt.Sprintf("Method %s not supported", request.Method))
|
||||||
|
}
|
@@ -1,82 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
var (
|
|
||||||
COND_EQ = ConditionType("eq")
|
|
||||||
COND_NE = ConditionType("ne")
|
|
||||||
COND_NULL = ConditionType("null")
|
|
||||||
COND_NOTNULL = ConditionType("notnull")
|
|
||||||
COND_IN = ConditionType("in")
|
|
||||||
COND_NOTIN = ConditionType("notin")
|
|
||||||
COND_OR = ConditionType("or")
|
|
||||||
COND_AND = ConditionType("AND")
|
|
||||||
|
|
||||||
mods = map[ConditionType]bool{
|
|
||||||
COND_EQ: true,
|
|
||||||
COND_NE: true,
|
|
||||||
COND_NULL: true,
|
|
||||||
COND_NOTNULL: true,
|
|
||||||
COND_IN: true,
|
|
||||||
COND_NOTIN: true,
|
|
||||||
COND_OR: true,
|
|
||||||
COND_AND: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConditionType string
|
|
||||||
|
|
||||||
type Condition struct {
|
|
||||||
values []interface{}
|
|
||||||
conditionType ConditionType
|
|
||||||
left, right *Condition
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidMod(mod string) bool {
|
|
||||||
return mods[ConditionType(mod)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCondition(conditionType ConditionType, values ...interface{}) *Condition {
|
|
||||||
return &Condition{
|
|
||||||
values: values,
|
|
||||||
conditionType: conditionType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NE(value interface{}) *Condition {
|
|
||||||
return NewCondition(COND_NE, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func EQ(value interface{}) *Condition {
|
|
||||||
return NewCondition(COND_EQ, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NULL(value interface{}) *Condition {
|
|
||||||
return NewCondition(COND_NULL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NOTNULL(value interface{}) *Condition {
|
|
||||||
return NewCondition(COND_NOTNULL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IN(values ...interface{}) *Condition {
|
|
||||||
return NewCondition(COND_IN, values...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NOTIN(values ...interface{}) *Condition {
|
|
||||||
return NewCondition(COND_NOTIN, values...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Condition) AND(right *Condition) *Condition {
|
|
||||||
return &Condition{
|
|
||||||
conditionType: COND_AND,
|
|
||||||
left: c,
|
|
||||||
right: right,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Condition) OR(right *Condition) *Condition {
|
|
||||||
return &Condition{
|
|
||||||
conditionType: COND_OR,
|
|
||||||
left: c,
|
|
||||||
right: right,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
package registry
|
|
||||||
|
|
||||||
import "github.com/rancher/go-rancher/v3"
|
|
||||||
|
|
||||||
type SchemaRegistry interface {
|
|
||||||
GetSchema(name string) *client.Schema
|
|
||||||
}
|
|
74
server/server.go
Normal file
74
server/server.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rancher/norman/api"
|
||||||
|
"github.com/rancher/norman/store/crd"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAPIServer(ctx context.Context, kubeConfig string, schemas *types.Schemas) (http.Handler, error) {
|
||||||
|
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to build kubeConfig")
|
||||||
|
}
|
||||||
|
return NewAPIServerFromConfig(ctx, config, schemas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClients(kubeConfig string) (*rest.RESTClient, clientset.Interface, error) {
|
||||||
|
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return NewClientsFromConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientsFromConfig(config *rest.Config) (*rest.RESTClient, clientset.Interface, error) {
|
||||||
|
dynamicConfig := *config
|
||||||
|
if dynamicConfig.NegotiatedSerializer == nil {
|
||||||
|
configConfig := dynamic.ContentConfig()
|
||||||
|
dynamicConfig.NegotiatedSerializer = configConfig.NegotiatedSerializer
|
||||||
|
}
|
||||||
|
|
||||||
|
k8sClient, err := rest.UnversionedRESTClientFor(&dynamicConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiExtClient, err := clientset.NewForConfig(&dynamicConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k8sClient, apiExtClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIServerFromConfig(ctx context.Context, config *rest.Config, schemas *types.Schemas) (http.Handler, error) {
|
||||||
|
k8sClient, apiExtClient, err := NewClientsFromConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewAPIServerFromClients(ctx, k8sClient, apiExtClient, schemas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIServerFromClients(ctx context.Context, k8sClient *rest.RESTClient, apiExtClient clientset.Interface, schemas *types.Schemas) (http.Handler, error) {
|
||||||
|
store := crd.NewCRDStore(apiExtClient, k8sClient)
|
||||||
|
if err := store.AddSchemas(ctx, schemas); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
server := api.NewAPIServer()
|
||||||
|
if err := server.AddSchemas(schemas); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := server.Start(ctx)
|
||||||
|
return server, err
|
||||||
|
}
|
389
store/crd/crd_store.go
Normal file
389
store/crd/crd_store.go
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
package crd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
|
apiextclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
schemas []*types.Schema
|
||||||
|
apiExtClientSet apiextclientset.Interface
|
||||||
|
k8sClient rest.Interface
|
||||||
|
schemaStatus map[*types.Schema]*apiext.CustomResourceDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCRDStore(apiExtClientSet apiextclientset.Interface, k8sClient rest.Interface) *Store {
|
||||||
|
return &Store{
|
||||||
|
apiExtClientSet: apiExtClientSet,
|
||||||
|
k8sClient: k8sClient,
|
||||||
|
schemaStatus: map[*types.Schema]*apiext.CustomResourceDefinition{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||||
|
crd, ok := c.schemaStatus[schema]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := ""
|
||||||
|
parts := strings.SplitN(id, ":", 2)
|
||||||
|
|
||||||
|
if len(parts) == 2 {
|
||||||
|
namespace = parts[0]
|
||||||
|
id = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
req := c.k8sClient.Get().
|
||||||
|
Prefix("apis", crd.Spec.Group, crd.Spec.Version).
|
||||||
|
Resource(crd.Status.AcceptedNames.Plural).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
if namespace != "" {
|
||||||
|
req.Namespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &unstructured.Unstructured{}
|
||||||
|
err := req.Do().Into(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.back(result.Object, schema)
|
||||||
|
|
||||||
|
return result.Object, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) back(data map[string]interface{}, schema *types.Schema) {
|
||||||
|
//mapping.Metadata.Back(data)
|
||||||
|
data["type"] = schema.ID
|
||||||
|
name, _ := data["name"].(string)
|
||||||
|
namespace, _ := data["namespace"].(string)
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
if namespace == "" {
|
||||||
|
data["id"] = name
|
||||||
|
} else {
|
||||||
|
data["id"] = namespace + ":" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) error {
|
||||||
|
crd, ok := c.schemaStatus[schema]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := ""
|
||||||
|
parts := strings.SplitN(id, ":", 2)
|
||||||
|
|
||||||
|
if len(parts) == 2 {
|
||||||
|
namespace = parts[0]
|
||||||
|
id = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
prop := metav1.DeletePropagationForeground
|
||||||
|
req := c.k8sClient.Delete().
|
||||||
|
Prefix("apis", crd.Spec.Group, crd.Spec.Version).
|
||||||
|
Resource(crd.Status.AcceptedNames.Plural).
|
||||||
|
Body(&metav1.DeleteOptions{
|
||||||
|
PropagationPolicy: &prop,
|
||||||
|
}).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
if namespace != "" {
|
||||||
|
req.Namespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &unstructured.Unstructured{}
|
||||||
|
return req.Do().Into(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||||
|
crd, ok := c.schemaStatus[schema]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req := c.k8sClient.Get().
|
||||||
|
Prefix("apis", crd.Spec.Group, crd.Spec.Version).
|
||||||
|
Resource(crd.Status.AcceptedNames.Plural)
|
||||||
|
|
||||||
|
resultList := &unstructured.UnstructuredList{}
|
||||||
|
err := req.Do().Into(resultList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, obj := range resultList.Items {
|
||||||
|
c.back(obj.Object, schema)
|
||||||
|
result = append(result, obj.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||||
|
crd, ok := c.schemaStatus[schema]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := ""
|
||||||
|
parts := strings.SplitN(id, ":", 2)
|
||||||
|
|
||||||
|
if len(parts) == 2 {
|
||||||
|
namespace = parts[0]
|
||||||
|
id = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
req := c.k8sClient.Get().
|
||||||
|
Prefix("apis", crd.Spec.Group, crd.Spec.Version).
|
||||||
|
Resource(crd.Status.AcceptedNames.Plural).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
if namespace != "" {
|
||||||
|
req.Namespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &unstructured.Unstructured{}
|
||||||
|
err := req.Do().Into(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//mapping.Metadata.Forward(data)
|
||||||
|
for k, v := range data {
|
||||||
|
if k == "metadata" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Object[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
req = c.k8sClient.Put().
|
||||||
|
Prefix("apis", crd.Spec.Group, crd.Spec.Version).
|
||||||
|
Resource(crd.Status.AcceptedNames.Plural).
|
||||||
|
Body(result).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
if namespace != "" {
|
||||||
|
req.Namespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &unstructured.Unstructured{}
|
||||||
|
err = req.Do().Into(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.back(result.Object, schema)
|
||||||
|
return result.Object, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
crd, ok := c.schemaStatus[schema]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, _ := data["namespace"].(string)
|
||||||
|
|
||||||
|
//mapping.Metadata.Forward(data)
|
||||||
|
|
||||||
|
data["apiVersion"] = crd.Spec.Group + "/" + crd.Spec.Version
|
||||||
|
data["kind"] = crd.Status.AcceptedNames.Kind
|
||||||
|
|
||||||
|
req := c.k8sClient.Post().
|
||||||
|
Prefix("apis", crd.Spec.Group, crd.Spec.Version).
|
||||||
|
Body(&unstructured.Unstructured{
|
||||||
|
Object: data,
|
||||||
|
}).
|
||||||
|
Resource(crd.Status.AcceptedNames.Plural)
|
||||||
|
|
||||||
|
if crd.Spec.Scope == apiext.NamespaceScoped {
|
||||||
|
req.Namespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &unstructured.Unstructured{}
|
||||||
|
err := req.Do().Into(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.back(result.Object, schema)
|
||||||
|
return result.Object, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) AddSchemas(ctx context.Context, schemas *types.Schemas) error {
|
||||||
|
if schemas.Err() != nil {
|
||||||
|
return schemas.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, schema := range schemas.Schemas() {
|
||||||
|
if schema.Store != nil || !contains(schema.CollectionMethods, http.MethodGet) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.Store = c
|
||||||
|
c.schemas = append(c.schemas, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
ready, err := c.getReadyCRDs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, schema := range c.schemas {
|
||||||
|
crd, err := c.createCRD(schema, ready)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.schemaStatus[schema] = crd
|
||||||
|
}
|
||||||
|
|
||||||
|
ready, err = c.getReadyCRDs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for schema, crd := range c.schemaStatus {
|
||||||
|
if _, ok := ready[crd.Name]; !ok {
|
||||||
|
if err := c.waitCRD(ctx, crd.Name, schema); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(list []string, s string) bool {
|
||||||
|
for _, i := range list {
|
||||||
|
if i == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) waitCRD(ctx context.Context, crdName string, schema *types.Schema) error {
|
||||||
|
logrus.Infof("Waiting for CRD %s to become available", crdName)
|
||||||
|
defer logrus.Infof("Done waiting for CRD %s to become available", crdName)
|
||||||
|
|
||||||
|
first := true
|
||||||
|
return wait.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) {
|
||||||
|
if !first {
|
||||||
|
logrus.Infof("Waiting for CRD %s to become available", crdName)
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
crd, err := c.apiExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cond := range crd.Status.Conditions {
|
||||||
|
switch cond.Type {
|
||||||
|
case apiext.Established:
|
||||||
|
if cond.Status == apiext.ConditionTrue {
|
||||||
|
c.schemaStatus[schema] = crd
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
case apiext.NamesAccepted:
|
||||||
|
if cond.Status == apiext.ConditionFalse {
|
||||||
|
logrus.Infof("Name conflict on %s: %v\n", crdName, cond.Reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ctx.Err()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) createCRD(schema *types.Schema, ready map[string]apiext.CustomResourceDefinition) (*apiext.CustomResourceDefinition, error) {
|
||||||
|
plural := strings.ToLower(schema.PluralName)
|
||||||
|
name := strings.ToLower(plural + "." + schema.Version.Group)
|
||||||
|
|
||||||
|
crd, ok := ready[name]
|
||||||
|
if ok {
|
||||||
|
return &crd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
crd = apiext.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: apiext.CustomResourceDefinitionSpec{
|
||||||
|
Group: schema.Version.Group,
|
||||||
|
Version: schema.Version.Version,
|
||||||
|
Scope: getScope(schema),
|
||||||
|
Names: apiext.CustomResourceDefinitionNames{
|
||||||
|
Plural: plural,
|
||||||
|
Kind: capitalize(schema.ID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("Creating CRD %s", name)
|
||||||
|
_, err := c.apiExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().Create(&crd)
|
||||||
|
if errors.IsAlreadyExists(err) {
|
||||||
|
return &crd, nil
|
||||||
|
}
|
||||||
|
return &crd, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Store) getReadyCRDs() (map[string]apiext.CustomResourceDefinition, error) {
|
||||||
|
list, err := c.apiExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]apiext.CustomResourceDefinition{}
|
||||||
|
|
||||||
|
for _, crd := range list.Items {
|
||||||
|
for _, cond := range crd.Status.Conditions {
|
||||||
|
switch cond.Type {
|
||||||
|
case apiext.Established:
|
||||||
|
if cond.Status == apiext.ConditionTrue {
|
||||||
|
result[crd.Name] = crd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScope(schema *types.Schema) apiext.ResourceScope {
|
||||||
|
for name := range schema.ResourceFields {
|
||||||
|
if name == "namespace" {
|
||||||
|
return apiext.NamespaceScoped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiext.ClusterScoped
|
||||||
|
}
|
||||||
|
|
||||||
|
func capitalize(s string) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(s[0:1]) + s[1:]
|
||||||
|
}
|
26
store/empty/empty_store.go
Normal file
26
store/empty/empty_store.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package empty
|
||||||
|
|
||||||
|
import "github.com/rancher/norman/types"
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
196
store/proxy/proxy_store.go
Normal file
196
store/proxy/proxy_store.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
k8sClient *rest.RESTClient
|
||||||
|
prefix []string
|
||||||
|
group string
|
||||||
|
version string
|
||||||
|
kind string
|
||||||
|
resourcePlural string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyStore(k8sClient *rest.RESTClient,
|
||||||
|
prefix []string, group, version, kind, resourcePlural string) *Store {
|
||||||
|
return &Store{
|
||||||
|
k8sClient: k8sClient,
|
||||||
|
prefix: prefix,
|
||||||
|
group: group,
|
||||||
|
version: version,
|
||||||
|
kind: kind,
|
||||||
|
resourcePlural: resourcePlural,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||||
|
namespace, id := splitID(id)
|
||||||
|
|
||||||
|
req := p.common(namespace, p.k8sClient.Get()).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
return p.singleResult(schema, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||||
|
namespace := getNamespace(apiContext, opt)
|
||||||
|
|
||||||
|
req := p.common(namespace, p.k8sClient.Get())
|
||||||
|
|
||||||
|
resultList := &unstructured.UnstructuredList{}
|
||||||
|
err := req.Do().Into(resultList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, obj := range resultList.Items {
|
||||||
|
result = append(result, p.fromInternal(schema, obj.Object))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNamespace(apiContext *types.APIContext, opt *types.QueryOptions) string {
|
||||||
|
if val, ok := apiContext.SubContext["namespace"]; ok {
|
||||||
|
return convert.ToString(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range opt.Conditions {
|
||||||
|
if condition.Field == "namespace" && len(condition.Values) > 0 {
|
||||||
|
return convert.ToString(condition.Values[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
namespace, _ := data["namespace"].(string)
|
||||||
|
p.toInternal(schema.Mapper, data)
|
||||||
|
|
||||||
|
req := p.common(namespace, p.k8sClient.Post()).
|
||||||
|
Body(&unstructured.Unstructured{
|
||||||
|
Object: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return p.singleResult(schema, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) toInternal(mapper types.Mapper, data map[string]interface{}) {
|
||||||
|
if mapper != nil {
|
||||||
|
mapper.ToInternal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.group == "" {
|
||||||
|
data["apiVersion"] = p.version
|
||||||
|
} else {
|
||||||
|
data["apiVersion"] = p.group + "/" + p.version
|
||||||
|
}
|
||||||
|
data["kind"] = p.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||||
|
existing, err := p.ByID(apiContext, schema, id)
|
||||||
|
if err != nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range data {
|
||||||
|
existing[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
p.toInternal(schema.Mapper, existing)
|
||||||
|
namespace, id := splitID(id)
|
||||||
|
|
||||||
|
req := p.common(namespace, p.k8sClient.Put()).
|
||||||
|
Body(&unstructured.Unstructured{
|
||||||
|
Object: existing,
|
||||||
|
}).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
return p.singleResult(schema, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) error {
|
||||||
|
namespace, id := splitID(id)
|
||||||
|
|
||||||
|
prop := metav1.DeletePropagationForeground
|
||||||
|
req := p.common(namespace, p.k8sClient.Delete()).
|
||||||
|
Body(&metav1.DeleteOptions{
|
||||||
|
PropagationPolicy: &prop,
|
||||||
|
}).
|
||||||
|
Name(id)
|
||||||
|
|
||||||
|
return req.Do().Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) singleResult(schema *types.Schema, req *rest.Request) (map[string]interface{}, error) {
|
||||||
|
result := &unstructured.Unstructured{}
|
||||||
|
err := req.Do().Into(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.fromInternal(schema, result.Object)
|
||||||
|
return result.Object, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitID(id string) (string, string) {
|
||||||
|
namespace := ""
|
||||||
|
parts := strings.SplitN(id, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
namespace = parts[0]
|
||||||
|
id = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespace, id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) common(namespace string, req *rest.Request) *rest.Request {
|
||||||
|
prefix := append([]string{}, p.prefix...)
|
||||||
|
if p.group != "" {
|
||||||
|
prefix = append(prefix, p.group)
|
||||||
|
}
|
||||||
|
prefix = append(prefix, p.version)
|
||||||
|
req.Prefix(prefix...).
|
||||||
|
Resource(p.resourcePlural)
|
||||||
|
|
||||||
|
if namespace != "" {
|
||||||
|
req.Namespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Store) fromInternal(schema *types.Schema, data map[string]interface{}) map[string]interface{} {
|
||||||
|
if schema.Mapper != nil {
|
||||||
|
schema.Mapper.FromInternal(data)
|
||||||
|
}
|
||||||
|
data["type"] = schema.ID
|
||||||
|
name, _ := data["name"].(string)
|
||||||
|
namespace, _ := data["namespace"].(string)
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
if namespace == "" {
|
||||||
|
data["id"] = name
|
||||||
|
} else {
|
||||||
|
data["id"] = namespace + ":" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
@@ -1,7 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
type ReferenceValidator interface {
|
|
||||||
|
|
||||||
Validate(resourceType, resourceID string) bool
|
|
||||||
|
|
||||||
}
|
|
48
store/schema/schema_store.go
Normal file
48
store/schema/schema_store.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/store/empty"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
empty.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchemaStore() types.Store {
|
||||||
|
return &Store{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||||
|
for _, schema := range apiContext.Schemas.Schemas() {
|
||||||
|
if strings.EqualFold(schema.ID, id) {
|
||||||
|
schemaData := map[string]interface{}{}
|
||||||
|
|
||||||
|
data, err := json.Marshal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaData, json.Unmarshal(data, &schemaData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||||
|
schemaData := []map[string]interface{}{}
|
||||||
|
|
||||||
|
data, err := json.Marshal(apiContext.Schemas.Schemas())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &schemaData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return schemaData, nil
|
||||||
|
}
|
108
types/condition.go
Normal file
108
types/condition.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
var (
|
||||||
|
COND_EQ = QueryConditionType{"eq", 1}
|
||||||
|
COND_NE = QueryConditionType{"ne", 1}
|
||||||
|
COND_NULL = QueryConditionType{"null", 0}
|
||||||
|
COND_NOTNULL = QueryConditionType{"notnull", 0}
|
||||||
|
COND_IN = QueryConditionType{"in", -1}
|
||||||
|
COND_NOTIN = QueryConditionType{"notin", -1}
|
||||||
|
COND_OR = QueryConditionType{"or", 1}
|
||||||
|
COND_AND = QueryConditionType{"and", 1}
|
||||||
|
|
||||||
|
mods = map[string]QueryConditionType{
|
||||||
|
COND_EQ.Name: COND_EQ,
|
||||||
|
COND_NE.Name: COND_NE,
|
||||||
|
COND_NULL.Name: COND_NULL,
|
||||||
|
COND_NOTNULL.Name: COND_NOTNULL,
|
||||||
|
COND_IN.Name: COND_IN,
|
||||||
|
COND_NOTIN.Name: COND_NOTIN,
|
||||||
|
COND_OR.Name: COND_OR,
|
||||||
|
COND_AND.Name: COND_AND,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryConditionType struct {
|
||||||
|
Name string
|
||||||
|
Args int
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryCondition struct {
|
||||||
|
Field string
|
||||||
|
Values []interface{}
|
||||||
|
conditionType QueryConditionType
|
||||||
|
left, right *QueryCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QueryCondition) ToCondition() Condition {
|
||||||
|
cond := Condition{
|
||||||
|
Modifier: q.conditionType.Name,
|
||||||
|
}
|
||||||
|
if q.conditionType.Args == 1 && len(q.Values) > 0 {
|
||||||
|
cond.Value = q.Values[0]
|
||||||
|
} else if q.conditionType.Args == -1 {
|
||||||
|
cond.Value = q.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidMod(mod string) bool {
|
||||||
|
_, ok := mods[mod]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConditionFromString(field, mod string, values ...interface{}) *QueryCondition {
|
||||||
|
return &QueryCondition{
|
||||||
|
Field: field,
|
||||||
|
Values: values,
|
||||||
|
conditionType: mods[mod],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCondition(mod QueryConditionType, values ...interface{}) *QueryCondition {
|
||||||
|
return &QueryCondition{
|
||||||
|
Values: values,
|
||||||
|
conditionType: mod,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NE(value interface{}) *QueryCondition {
|
||||||
|
return NewCondition(COND_NE, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EQ(value interface{}) *QueryCondition {
|
||||||
|
return NewCondition(COND_EQ, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NULL(value interface{}) *QueryCondition {
|
||||||
|
return NewCondition(COND_NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NOTNULL(value interface{}) *QueryCondition {
|
||||||
|
return NewCondition(COND_NOTNULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IN(values ...interface{}) *QueryCondition {
|
||||||
|
return NewCondition(COND_IN, values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NOTIN(values ...interface{}) *QueryCondition {
|
||||||
|
return NewCondition(COND_NOTIN, values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *QueryCondition) AND(right *QueryCondition) *QueryCondition {
|
||||||
|
return &QueryCondition{
|
||||||
|
conditionType: COND_AND,
|
||||||
|
left: c,
|
||||||
|
right: right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *QueryCondition) OR(right *QueryCondition) *QueryCondition {
|
||||||
|
return &QueryCondition{
|
||||||
|
conditionType: COND_OR,
|
||||||
|
left: c,
|
||||||
|
right: right,
|
||||||
|
}
|
||||||
|
}
|
150
types/convert/convert.go
Normal file
150
types/convert/convert.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Singular(value interface{}) interface{} {
|
||||||
|
if slice, ok := value.([]string); ok {
|
||||||
|
if len(slice) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return slice[0]
|
||||||
|
}
|
||||||
|
if slice, ok := value.([]interface{}); ok {
|
||||||
|
if len(slice) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return slice[0]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToString(value interface{}) string {
|
||||||
|
single := Singular(value)
|
||||||
|
if single == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprint(single)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTimestamp(value interface{}) (int64, error) {
|
||||||
|
str := ToString(value)
|
||||||
|
if str == "" {
|
||||||
|
return 0, errors.New("Invalid date")
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, str)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return t.UnixNano() / 1000000, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToBool(value interface{}) bool {
|
||||||
|
value = Singular(value)
|
||||||
|
|
||||||
|
b, ok := value.(bool)
|
||||||
|
if ok {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
str := strings.ToLower(ToString(value))
|
||||||
|
return str == "true" || str == "t" || str == "yes" || str == "y"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToNumber(value interface{}) (int64, error) {
|
||||||
|
value = Singular(value)
|
||||||
|
|
||||||
|
i, ok := value.(int64)
|
||||||
|
if ok {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
return strconv.ParseInt(ToString(value), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Capitalize(s string) string {
|
||||||
|
if len(s) <= 1 {
|
||||||
|
return strings.ToUpper(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(s[:1]) + s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func LowerTitle(input string) string {
|
||||||
|
runes := []rune(input)
|
||||||
|
for i := 0; i < len(runes); i++ {
|
||||||
|
if unicode.IsUpper(runes[i]) &&
|
||||||
|
(i == 0 ||
|
||||||
|
i == len(runes)-1 ||
|
||||||
|
unicode.IsUpper(runes[i+1])) {
|
||||||
|
runes[i] = unicode.ToLower(runes[i])
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsEmpty(v interface{}) bool {
|
||||||
|
return v == nil || v == "" || v == 0 || v == false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMapInterface(obj interface{}) map[string]interface{} {
|
||||||
|
v, _ := obj.(map[string]interface{})
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMapSlice(obj interface{}) []map[string]interface{} {
|
||||||
|
if v, ok := obj.([]map[string]interface{}); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
vs, _ := obj.([]interface{})
|
||||||
|
result := []map[string]interface{}{}
|
||||||
|
for _, item := range vs {
|
||||||
|
if v, ok := item.(map[string]interface{}); ok {
|
||||||
|
result = append(result, v)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToStringSlice(data interface{}) []string {
|
||||||
|
if v, ok := data.([]string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if v, ok := data.([]interface{}); ok {
|
||||||
|
result := []string{}
|
||||||
|
for _, item := range v {
|
||||||
|
result = append(result, ToString(item))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToObj(data interface{}, obj interface{}) error {
|
||||||
|
bytes, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(bytes, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeToMap(obj interface{}) (map[string]interface{}, error) {
|
||||||
|
bytes, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
return result, json.Unmarshal(bytes, result)
|
||||||
|
}
|
19
types/convert/value_set_string.go
Normal file
19
types/convert/value_set_string.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
splitRegexp = regexp.MustCompile("[[:space:]]*,[[:space:]]*")
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToValuesSlice(value string) []string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if strings.HasPrefix(value, "(") && strings.HasSuffix(value, ")") {
|
||||||
|
return splitRegexp.Split(value[1:len(value)-1], -1)
|
||||||
|
} else {
|
||||||
|
return []string{value}
|
||||||
|
}
|
||||||
|
}
|
24
types/definition/definition.go
Normal file
24
types/definition/definition.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func IsMapType(fieldType string) bool {
|
||||||
|
return strings.HasPrefix(fieldType, "map[") && strings.HasSuffix(fieldType, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsArrayType(fieldType string) bool {
|
||||||
|
return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsReferenceType(fieldType string) bool {
|
||||||
|
return strings.HasPrefix(fieldType, "reference[") && strings.HasSuffix(fieldType, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubType(fieldType string) string {
|
||||||
|
i := strings.Index(fieldType, "[")
|
||||||
|
if i <= 0 || i >= len(fieldType)-1 {
|
||||||
|
return fieldType
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldType[i+1 : len(fieldType)-1]
|
||||||
|
}
|
104
types/mapper.go
Normal file
104
types/mapper.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rancher/norman/types/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mapper interface {
|
||||||
|
FromInternal(data map[string]interface{})
|
||||||
|
ToInternal(data map[string]interface{})
|
||||||
|
ModifySchema(schema *Schema, schemas *Schemas) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeMapper struct {
|
||||||
|
Mappers []Mapper
|
||||||
|
typeName string
|
||||||
|
subSchemas map[string]*Schema
|
||||||
|
subArraySchemas map[string]*Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeMapper) FromInternal(data map[string]interface{}) {
|
||||||
|
for fieldName, schema := range t.subSchemas {
|
||||||
|
if schema.Mapper == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldData, _ := data[fieldName].(map[string]interface{})
|
||||||
|
schema.Mapper.FromInternal(fieldData)
|
||||||
|
}
|
||||||
|
|
||||||
|
for fieldName, schema := range t.subArraySchemas {
|
||||||
|
if schema.Mapper == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
datas, _ := data[fieldName].([]interface{})
|
||||||
|
for _, fieldData := range datas {
|
||||||
|
mapFieldData, _ := fieldData.(map[string]interface{})
|
||||||
|
schema.Mapper.FromInternal(mapFieldData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mapper := range t.Mappers {
|
||||||
|
mapper.FromInternal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data != nil {
|
||||||
|
data["type"] = t.typeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeMapper) ToInternal(data map[string]interface{}) {
|
||||||
|
for i := len(t.Mappers) - 1; i <= 0; i-- {
|
||||||
|
t.Mappers[i].ToInternal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for fieldName, schema := range t.subArraySchemas {
|
||||||
|
if schema.Mapper == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
datas, _ := data[fieldName].([]map[string]interface{})
|
||||||
|
for _, fieldData := range datas {
|
||||||
|
schema.Mapper.ToInternal(fieldData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for fieldName, schema := range t.subSchemas {
|
||||||
|
if schema.Mapper == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldData, _ := data[fieldName].(map[string]interface{})
|
||||||
|
schema.Mapper.ToInternal(fieldData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TypeMapper) ModifySchema(schema *Schema, schemas *Schemas) error {
|
||||||
|
t.subSchemas = map[string]*Schema{}
|
||||||
|
t.subArraySchemas = map[string]*Schema{}
|
||||||
|
t.typeName = schema.ID
|
||||||
|
|
||||||
|
mapperSchema := schema
|
||||||
|
if schema.InternalSchema != nil {
|
||||||
|
mapperSchema = schema.InternalSchema
|
||||||
|
}
|
||||||
|
for name, field := range mapperSchema.ResourceFields {
|
||||||
|
fieldType := field.Type
|
||||||
|
targetMap := t.subSchemas
|
||||||
|
if definition.IsArrayType(fieldType) {
|
||||||
|
fieldType = definition.SubType(fieldType)
|
||||||
|
targetMap = t.subArraySchemas
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := schemas.Schema(&schema.Version, fieldType)
|
||||||
|
if schema != nil {
|
||||||
|
targetMap[name] = schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mapper := range t.Mappers {
|
||||||
|
if err := mapper.ModifySchema(schema, schemas); err != nil {
|
||||||
|
return errors.Wrapf(err, "mapping type %s", schema.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
49
types/mapping/mapper.go
Normal file
49
types/mapping/mapper.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package mapping
|
||||||
|
|
||||||
|
var (
|
||||||
|
//Metadata m.Mapper = m.CombinedMapper{
|
||||||
|
// Mappers: []m.Mapper{
|
||||||
|
// m.Enum{
|
||||||
|
// From: "name",
|
||||||
|
// To: "metadata/name",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "uuid",
|
||||||
|
// To: "metadata/uid",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "resourceVersion",
|
||||||
|
// To: "metadata/resourceVersion",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "created",
|
||||||
|
// To: "metadata/creationTimestamp",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "removed",
|
||||||
|
// To: "metadata/deletionTimestamp",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "namespace",
|
||||||
|
// To: "metadata/namespace",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "labels",
|
||||||
|
// To: "metadata/labels",
|
||||||
|
// },
|
||||||
|
// m.Enum{
|
||||||
|
// From: "annotations",
|
||||||
|
// To: "metadata/annotations",
|
||||||
|
// },
|
||||||
|
// m.Swap{
|
||||||
|
// Left: "type",
|
||||||
|
// Right: "kind",
|
||||||
|
// },
|
||||||
|
// m.LabelField{
|
||||||
|
// Fields: []string{
|
||||||
|
// "description",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
//}
|
||||||
|
)
|
28
types/mapping/mapper/check.go
Normal file
28
types/mapping/mapper/check.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInternal(schema *types.Schema) (*types.Schema, error) {
|
||||||
|
if schema.InternalSchema == nil {
|
||||||
|
return nil, fmt.Errorf("no internal schema found for schema %s", schema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.InternalSchema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateInternalField(field string, schema *types.Schema) (*types.Schema, error) {
|
||||||
|
internalSchema, err := getInternal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := internalSchema.ResourceFields[field]; !ok {
|
||||||
|
return nil, fmt.Errorf("field %s missing on internal schema %s", field, schema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return internalSchema, nil
|
||||||
|
}
|
25
types/mapping/mapper/copy.go
Normal file
25
types/mapping/mapper/copy.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type Copy struct {
|
||||||
|
From, To string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Copy) Forward(data map[string]interface{}) {
|
||||||
|
val, ok := GetValue(data, strings.Split(c.From, "/")...)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PutValue(data, val, strings.Split(c.To, "/")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Copy) Back(data map[string]interface{}) {
|
||||||
|
val, ok := GetValue(data, strings.Split(c.To, "/")...)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PutValue(data, val, strings.Split(c.From, "/")...)
|
||||||
|
}
|
32
types/mapping/mapper/drop.go
Normal file
32
types/mapping/mapper/drop.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Drop struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Drop) FromInternal(data map[string]interface{}) {
|
||||||
|
delete(data, d.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Drop) ToInternal(data map[string]interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Drop) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
_, err := getInternal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := schema.ResourceFields[d.Field]; !ok {
|
||||||
|
return fmt.Errorf("can not drop missing field %s on %s", d.Field, schema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(schema.ResourceFields, d.Field)
|
||||||
|
return nil
|
||||||
|
}
|
65
types/mapping/mapper/embed.go
Normal file
65
types/mapping/mapper/embed.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Embed struct {
|
||||||
|
Field string
|
||||||
|
ignoreOverride bool
|
||||||
|
embeddedFields []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Embed) FromInternal(data map[string]interface{}) {
|
||||||
|
sub, _ := data[e.Field].(map[string]interface{})
|
||||||
|
for _, fieldName := range e.embeddedFields {
|
||||||
|
if v, ok := sub[fieldName]; ok {
|
||||||
|
data[fieldName] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(data, e.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Embed) ToInternal(data map[string]interface{}) {
|
||||||
|
sub := map[string]interface{}{}
|
||||||
|
for _, fieldName := range e.embeddedFields {
|
||||||
|
if v, ok := data[fieldName]; ok {
|
||||||
|
sub[fieldName] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(data, fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
data[e.Field] = sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Embed) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
internalSchema, err := validateInternalField(e.Field, schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.embeddedFields = []string{}
|
||||||
|
|
||||||
|
embeddedSchemaID := internalSchema.ResourceFields[e.Field].Type
|
||||||
|
embeddedSchema := schemas.Schema(&schema.Version, embeddedSchemaID)
|
||||||
|
if embeddedSchema == nil {
|
||||||
|
return fmt.Errorf("failed to find schema %s for embedding", embeddedSchemaID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, field := range embeddedSchema.ResourceFields {
|
||||||
|
if !e.ignoreOverride {
|
||||||
|
if _, ok := schema.ResourceFields[name]; ok {
|
||||||
|
return fmt.Errorf("embedding field %s on %s will overwrite the field %s",
|
||||||
|
e.Field, schema.ID, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema.ResourceFields[name] = field
|
||||||
|
e.embeddedFields = append(e.embeddedFields, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(schema.ResourceFields, e.Field)
|
||||||
|
return nil
|
||||||
|
}
|
49
types/mapping/mapper/enum.go
Normal file
49
types/mapping/mapper/enum.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Enum struct {
|
||||||
|
Field string
|
||||||
|
Values map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Enum) FromInternal(data map[string]interface{}) {
|
||||||
|
v, ok := data[e.Field]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
str := convert.ToString(v)
|
||||||
|
|
||||||
|
mapping, ok := e.Values[str]
|
||||||
|
if ok {
|
||||||
|
data[e.Field] = mapping[0]
|
||||||
|
} else {
|
||||||
|
data[e.Field] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Enum) ToInternal(data map[string]interface{}) {
|
||||||
|
v, ok := data[e.Field]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
str := convert.ToString(v)
|
||||||
|
for newValue, values := range e.Values {
|
||||||
|
for _, value := range values {
|
||||||
|
if str == value {
|
||||||
|
data[e.Field] = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Enum) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
_, err := validateInternalField(e.Field, schema)
|
||||||
|
return err
|
||||||
|
}
|
25
types/mapping/mapper/label_field.go
Normal file
25
types/mapping/mapper/label_field.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
//type LabelField struct {
|
||||||
|
// Fields []string
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func (l LabelField) Forward(data map[string]interface{}) {
|
||||||
|
// for _, field := range l.Fields {
|
||||||
|
// moveForLabel(field).Forward(data)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func (l LabelField) Back(data map[string]interface{}) {
|
||||||
|
// for _, field := range l.Fields {
|
||||||
|
// moveForLabel(field).Back(data)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func moveForLabel(field string) *Enum {
|
||||||
|
// return &Enum{
|
||||||
|
// From: field,
|
||||||
|
// To: "metadata/labels/io.cattle.field." + strings.ToLower(field),
|
||||||
|
// }
|
||||||
|
//}
|
48
types/mapping/mapper/move.go
Normal file
48
types/mapping/mapper/move.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Move struct {
|
||||||
|
From, To string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Move) FromInternal(data map[string]interface{}) {
|
||||||
|
if v, ok := GetValue(data, m.From); ok {
|
||||||
|
data[m.To] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Move) ToInternal(data map[string]interface{}) {
|
||||||
|
if v, ok := GetValue(data, m.To); ok {
|
||||||
|
data[m.From] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Move) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
internalSchema, err := getInternal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field, ok := internalSchema.ResourceFields[m.From]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("missing field %s on internal schema %s", m.From, internalSchema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = schema.ResourceFields[m.To]
|
||||||
|
if ok {
|
||||||
|
return fmt.Errorf("field %s already exists on schema %s", m.From, internalSchema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(schema.ResourceFields, m.From)
|
||||||
|
|
||||||
|
field.CodeName = convert.Capitalize(m.To)
|
||||||
|
schema.ResourceFields[m.To] = field
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
60
types/mapping/mapper/slice_to_map.go
Normal file
60
types/mapping/mapper/slice_to_map.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SliceToMap struct {
|
||||||
|
Field string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SliceToMap) FromInternal(data map[string]interface{}) {
|
||||||
|
datas, _ := data[s.Field].([]interface{})
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, item := range datas {
|
||||||
|
if mapItem, ok := item.(map[string]interface{}); ok {
|
||||||
|
name, _ := mapItem[s.Key].(string)
|
||||||
|
delete(mapItem, s.Key)
|
||||||
|
result[name] = mapItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data[s.Field] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SliceToMap) ToInternal(data map[string]interface{}) {
|
||||||
|
datas, _ := data[s.Field].(map[string]interface{})
|
||||||
|
result := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for name, item := range datas {
|
||||||
|
mapItem, _ := item.(map[string]interface{})
|
||||||
|
if mapItem != nil {
|
||||||
|
mapItem[s.Key] = name
|
||||||
|
result = append(result, mapItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data[s.Field] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SliceToMap) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
internalSchema, err := validateInternalField(s.Field, schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field := internalSchema.ResourceFields[s.Field]
|
||||||
|
if !definition.IsArrayType(field.Type) {
|
||||||
|
return fmt.Errorf("field %s on %s is not an array", s.Field, internalSchema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Type = "map[" + definition.SubType(field.Type) + "]"
|
||||||
|
schema.ResourceFields[s.Field] = field
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
20
types/mapping/mapper/swap.go
Normal file
20
types/mapping/mapper/swap.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
type Swap struct {
|
||||||
|
Left, Right string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Swap) Forward(data map[string]interface{}) {
|
||||||
|
rightValue, rightOk := data[s.Right]
|
||||||
|
leftValue, leftOk := data[s.Left]
|
||||||
|
if rightOk {
|
||||||
|
data[s.Left] = rightValue
|
||||||
|
}
|
||||||
|
if leftOk {
|
||||||
|
data[s.Right] = leftValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Swap) Back(data map[string]interface{}) {
|
||||||
|
s.Forward(data)
|
||||||
|
}
|
1
types/mapping/mapper/type.go
Normal file
1
types/mapping/mapper/type.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package mapper
|
68
types/mapping/mapper/union_embed.go
Normal file
68
types/mapping/mapper/union_embed.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnionMapping struct {
|
||||||
|
FieldName string
|
||||||
|
CheckFields []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionEmbed struct {
|
||||||
|
Fields []UnionMapping
|
||||||
|
embeds map[string]Embed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnionEmbed) FromInternal(data map[string]interface{}) {
|
||||||
|
for _, embed := range u.embeds {
|
||||||
|
embed.FromInternal(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnionEmbed) ToInternal(data map[string]interface{}) {
|
||||||
|
outer:
|
||||||
|
for _, mapper := range u.Fields {
|
||||||
|
if len(mapper.CheckFields) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, check := range mapper.CheckFields {
|
||||||
|
v, ok := data[check]
|
||||||
|
if !ok || convert.IsEmpty(v) {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
embed := u.embeds[mapper.FieldName]
|
||||||
|
embed.ToInternal(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnionEmbed) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||||
|
u.embeds = map[string]Embed{}
|
||||||
|
|
||||||
|
for _, mapping := range u.Fields {
|
||||||
|
embed := Embed{
|
||||||
|
Field: mapping.FieldName,
|
||||||
|
ignoreOverride: true,
|
||||||
|
}
|
||||||
|
if err := embed.ModifySchema(schema, schemas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, checkField := range mapping.CheckFields {
|
||||||
|
if _, ok := schema.ResourceFields[checkField]; !ok {
|
||||||
|
return fmt.Errorf("missing check field %s on schema %s", checkField, schema.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.embeds[mapping.FieldName] = embed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
83
types/mapping/mapper/values.go
Normal file
83
types/mapping/mapper/values.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
func RemoveValue(data map[string]interface{}, keys ...string) (interface{}, bool) {
|
||||||
|
for i, key := range keys {
|
||||||
|
if i == len(keys)-1 {
|
||||||
|
val, ok := data[key]
|
||||||
|
delete(data, key)
|
||||||
|
return val, ok
|
||||||
|
} else {
|
||||||
|
data, _ = data[key].(map[string]interface{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSlice(data map[string]interface{}, keys ...string) ([]map[string]interface{}, bool) {
|
||||||
|
val, ok := GetValue(data, keys...)
|
||||||
|
if !ok {
|
||||||
|
return nil, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, typeOk := val.([]map[string]interface{})
|
||||||
|
if typeOk {
|
||||||
|
return slice, typeOk
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceNext, typeOk := val.([]interface{})
|
||||||
|
if !typeOk {
|
||||||
|
return nil, typeOk
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []map[string]interface{}{}
|
||||||
|
for _, val := range sliceNext {
|
||||||
|
if v, ok := val.(map[string]interface{}); ok {
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetValueN(data map[string]interface{}, keys ...string) interface{} {
|
||||||
|
val, _ := GetValue(data, keys...)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetValue(data map[string]interface{}, keys ...string) (interface{}, bool) {
|
||||||
|
for i, key := range keys {
|
||||||
|
if i == len(keys)-1 {
|
||||||
|
val, ok := data[key]
|
||||||
|
return val, ok
|
||||||
|
} else {
|
||||||
|
data, _ = data[key].(map[string]interface{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutValue(data map[string]interface{}, val interface{}, keys ...string) {
|
||||||
|
// This is so ugly
|
||||||
|
for i, key := range keys {
|
||||||
|
if i == len(keys)-1 {
|
||||||
|
data[key] = val
|
||||||
|
} else {
|
||||||
|
newData, ok := data[key]
|
||||||
|
if ok {
|
||||||
|
newMap, ok := newData.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
data = newMap
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newMap := map[string]interface{}{}
|
||||||
|
data[key] = newMap
|
||||||
|
data = newMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
326
types/reflection.go
Normal file
326
types/reflection.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
resourceType = reflect.TypeOf(Resource{})
|
||||||
|
metaType = reflect.TypeOf(metav1.ObjectMeta{})
|
||||||
|
blacklistNames = map[string]bool{
|
||||||
|
"links": true,
|
||||||
|
"actions": true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Schemas) AddMapperForType(version *APIVersion, obj interface{}, mapper Mapper) *Schemas {
|
||||||
|
t := reflect.TypeOf(obj)
|
||||||
|
typeName := convert.LowerTitle(t.Name())
|
||||||
|
return s.AddMapper(version, typeName, mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) MustImport(version *APIVersion, obj interface{}, externalOverrides ...interface{}) *Schemas {
|
||||||
|
//TODO: remove
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
if _, err := s.Import(version, obj, externalOverrides...); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) Import(version *APIVersion, obj interface{}, externalOverrides ...interface{}) (*Schema, error) {
|
||||||
|
types := []reflect.Type{}
|
||||||
|
for _, override := range externalOverrides {
|
||||||
|
types = append(types, reflect.TypeOf(override))
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.importType(version, reflect.TypeOf(obj), types...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) newSchemaFromType(version *APIVersion, t reflect.Type, typeName string) (*Schema, error) {
|
||||||
|
schema := &Schema{
|
||||||
|
ID: typeName,
|
||||||
|
Version: *version,
|
||||||
|
CodeName: t.Name(),
|
||||||
|
ResourceFields: map[string]Field{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.readFields(schema, t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) importType(version *APIVersion, t reflect.Type, overrides ...reflect.Type) (*Schema, error) {
|
||||||
|
typeName := convert.LowerTitle(t.Name())
|
||||||
|
|
||||||
|
existing := s.Schema(version, typeName)
|
||||||
|
if existing != nil {
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Inspecting schema %s for %v", typeName, t)
|
||||||
|
|
||||||
|
schema, err := s.newSchemaFromType(version, t, typeName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mapper := s.mapper(&schema.Version, schema.ID)
|
||||||
|
if mapper != nil {
|
||||||
|
copy, err := s.newSchemaFromType(version, t, typeName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
schema.InternalSchema = copy
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, override := range overrides {
|
||||||
|
if err := s.readFields(schema, override); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapper == nil {
|
||||||
|
mapper = &TypeMapper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mapper.ModifySchema(schema, s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.Mapper = mapper
|
||||||
|
s.AddSchema(schema)
|
||||||
|
|
||||||
|
return schema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonName(f reflect.StructField) string {
|
||||||
|
return strings.SplitN(f.Tag.Get("json"), ",", 2)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) readFields(schema *Schema, t reflect.Type) error {
|
||||||
|
if t == resourceType {
|
||||||
|
schema.CollectionMethods = []string{"GET", "POST"}
|
||||||
|
schema.ResourceMethods = []string{"GET", "PUT", "DELETE"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
|
||||||
|
if field.PkgPath != "" {
|
||||||
|
// unexported field
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonName := jsonName(field)
|
||||||
|
|
||||||
|
if jsonName == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Anonymous && jsonName == "" {
|
||||||
|
t := field.Type
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t.Kind() == reflect.Struct {
|
||||||
|
if err := s.readFields(schema, t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldName := jsonName
|
||||||
|
if fieldName == "" {
|
||||||
|
fieldName = convert.LowerTitle(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blacklistNames[fieldName] {
|
||||||
|
logrus.Debugf("Ignoring blacklisted field %s.%s for %v", schema.ID, fieldName, field)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Inspecting field %s.%s for %v", schema.ID, fieldName, field)
|
||||||
|
|
||||||
|
schemaField := Field{
|
||||||
|
Create: true,
|
||||||
|
Update: true,
|
||||||
|
CodeName: field.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldType := field.Type
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
schemaField.Nullable = true
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applyTag(&field, &schemaField); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if schemaField.Type == "" {
|
||||||
|
inferedType, err := s.determineSchemaType(&schema.Version, fieldType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
schemaField.Type = inferedType
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Type == metaType {
|
||||||
|
schema.CollectionMethods = []string{"GET", "POST"}
|
||||||
|
schema.ResourceMethods = []string{"GET", "PUT", "DELETE"}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Setting field %s.%s: %#v", schema.ID, fieldName, schemaField)
|
||||||
|
schema.ResourceFields[fieldName] = schemaField
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTag(structField *reflect.StructField, field *Field) error {
|
||||||
|
for _, part := range strings.Split(structField.Tag.Get("norman"), ",") {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
key, value := getKeyValue(part)
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
field.Type = value
|
||||||
|
case "codeName":
|
||||||
|
field.CodeName = value
|
||||||
|
case "default":
|
||||||
|
field.Default = value
|
||||||
|
case "nullabled":
|
||||||
|
field.Nullable = true
|
||||||
|
case "nocreate":
|
||||||
|
field.Create = false
|
||||||
|
case "writeOnly":
|
||||||
|
field.WriteOnly = true
|
||||||
|
case "required":
|
||||||
|
field.Required = true
|
||||||
|
case "noupdate":
|
||||||
|
field.Update = false
|
||||||
|
case "minLength":
|
||||||
|
field.MinLength, err = toInt(value, structField)
|
||||||
|
case "maxLength":
|
||||||
|
field.MaxLength, err = toInt(value, structField)
|
||||||
|
case "min":
|
||||||
|
field.Min, err = toInt(value, structField)
|
||||||
|
case "max":
|
||||||
|
field.Max, err = toInt(value, structField)
|
||||||
|
case "options":
|
||||||
|
field.Options = split(value)
|
||||||
|
if field.Type == "" {
|
||||||
|
field.Type = "enum"
|
||||||
|
}
|
||||||
|
case "validChars":
|
||||||
|
field.ValidChars = value
|
||||||
|
case "invalidChars":
|
||||||
|
field.InvalidChars = value
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid tag %s on field %s", key, structField.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(value string, structField *reflect.StructField) (*int64, error) {
|
||||||
|
i, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid number on field %s: %v", structField.Name, err)
|
||||||
|
}
|
||||||
|
return &i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func split(input string) []string {
|
||||||
|
result := []string{}
|
||||||
|
for _, i := range strings.Split(input, "|") {
|
||||||
|
for _, part := range strings.Split(i, " ") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if len(part) > 0 {
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyValue(input string) (string, string) {
|
||||||
|
var (
|
||||||
|
key, value string
|
||||||
|
)
|
||||||
|
parts := strings.SplitN(input, "=", 2)
|
||||||
|
key = parts[0]
|
||||||
|
if len(parts) > 1 {
|
||||||
|
value = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) determineSchemaType(version *APIVersion, t reflect.Type) (string, error) {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean", nil
|
||||||
|
case reflect.Int:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Int32:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Int64:
|
||||||
|
return "int", nil
|
||||||
|
case reflect.Interface:
|
||||||
|
return "json", nil
|
||||||
|
case reflect.Map:
|
||||||
|
subType, err := s.determineSchemaType(version, t.Elem())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("map[%s]", subType), nil
|
||||||
|
case reflect.Slice:
|
||||||
|
subType, err := s.determineSchemaType(version, t.Elem())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("array[%s]", subType), nil
|
||||||
|
case reflect.String:
|
||||||
|
return "string", nil
|
||||||
|
case reflect.Struct:
|
||||||
|
if t.Name() == "Time" {
|
||||||
|
return "date", nil
|
||||||
|
}
|
||||||
|
if t.Name() == "IntOrString" {
|
||||||
|
return "string", nil
|
||||||
|
}
|
||||||
|
if t.Name() == "Quantity" {
|
||||||
|
return "string", nil
|
||||||
|
}
|
||||||
|
schema, err := s.importType(version, t)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return schema.ID, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown type kind %s", t.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
180
types/schemas.go
Normal file
180
types/schemas.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/name"
|
||||||
|
"github.com/rancher/norman/types/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemaCollection struct {
|
||||||
|
Data []Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schemas struct {
|
||||||
|
schemasByPath map[string]map[string]*Schema
|
||||||
|
mappers map[string]map[string]Mapper
|
||||||
|
versions []APIVersion
|
||||||
|
schemas []*Schema
|
||||||
|
errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchemas() *Schemas {
|
||||||
|
return &Schemas{
|
||||||
|
schemasByPath: map[string]map[string]*Schema{},
|
||||||
|
mappers: map[string]map[string]Mapper{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) Err() error {
|
||||||
|
return NewErrors(s.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) AddSchema(schema *Schema) *Schemas {
|
||||||
|
schema.Type = "schema"
|
||||||
|
if schema.ID == "" {
|
||||||
|
s.errors = append(s.errors, fmt.Errorf("ID is not set on schema: %v", schema))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if schema.Version.Path == "" || schema.Version.Group == "" || schema.Version.Version == "" {
|
||||||
|
s.errors = append(s.errors, fmt.Errorf("version is not set on schema: %s", schema.ID))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if schema.PluralName == "" {
|
||||||
|
schema.PluralName = name.GuessPluralName(schema.ID)
|
||||||
|
}
|
||||||
|
if schema.CodeName == "" {
|
||||||
|
schema.CodeName = convert.Capitalize(schema.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
schemas, ok := s.schemasByPath[schema.Version.Path]
|
||||||
|
if !ok {
|
||||||
|
schemas = map[string]*Schema{}
|
||||||
|
s.schemasByPath[schema.Version.Path] = schemas
|
||||||
|
s.versions = append(s.versions, schema.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := schemas[schema.ID]; !ok {
|
||||||
|
schemas[schema.ID] = schema
|
||||||
|
s.schemas = append(s.schemas, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) AddMapper(version *APIVersion, schemaID string, mapper Mapper) *Schemas {
|
||||||
|
mappers, ok := s.mappers[version.Path]
|
||||||
|
if !ok {
|
||||||
|
mappers = map[string]Mapper{}
|
||||||
|
s.mappers[version.Path] = mappers
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := mappers[schemaID]; !ok {
|
||||||
|
mappers[schemaID] = mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) SchemasForVersion(version APIVersion) map[string]*Schema {
|
||||||
|
return s.schemasByPath[version.Path]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) Versions() []APIVersion {
|
||||||
|
return s.versions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) Schemas() []*Schema {
|
||||||
|
return s.schemas
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) mapper(version *APIVersion, name string) Mapper {
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
)
|
||||||
|
|
||||||
|
if strings.Contains(name, "/") {
|
||||||
|
idx := strings.LastIndex(name, "/")
|
||||||
|
path = name[0:idx]
|
||||||
|
name = name[idx+1:]
|
||||||
|
} else if version != nil {
|
||||||
|
path = version.Path
|
||||||
|
} else {
|
||||||
|
path = "core"
|
||||||
|
}
|
||||||
|
|
||||||
|
mappers, ok := s.mappers[path]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mapper := mappers[name]
|
||||||
|
if mapper != nil {
|
||||||
|
return mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schemas) Schema(version *APIVersion, name string) *Schema {
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
)
|
||||||
|
|
||||||
|
if strings.Contains(name, "/") {
|
||||||
|
idx := strings.LastIndex(name, "/")
|
||||||
|
path = name[0:idx]
|
||||||
|
name = name[idx+1:]
|
||||||
|
} else if version != nil {
|
||||||
|
path = version.Path
|
||||||
|
} else {
|
||||||
|
path = "core"
|
||||||
|
}
|
||||||
|
|
||||||
|
schemas, ok := s.schemasByPath[path]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := schemas[name]
|
||||||
|
if schema != nil {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, check := range schemas {
|
||||||
|
if strings.EqualFold(check.ID, name) || strings.EqualFold(check.PluralName, name) {
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiErrors struct {
|
||||||
|
errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrors(errors []error) error {
|
||||||
|
if len(errors) == 0 {
|
||||||
|
return nil
|
||||||
|
} else if len(errors) == 1 {
|
||||||
|
return errors[0]
|
||||||
|
}
|
||||||
|
return &multiErrors{
|
||||||
|
errors: errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiErrors) Error() string {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
for _, err := range m.errors {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
buf.WriteString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
120
types/server_types.go
Normal file
120
types/server_types.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValuesMap struct {
|
||||||
|
Foo map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawResource struct {
|
||||||
|
ID string `json:"id,omitempty" yaml:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
||||||
|
Schema *Schema `json:"-" yaml:"-"`
|
||||||
|
Links map[string]string `json:"links" yaml:"links"`
|
||||||
|
Actions map[string]string `json:"actions" yaml:"actions"`
|
||||||
|
Values map[string]interface{} `json:",inline"`
|
||||||
|
ActionLinks bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawResource) MarshalJSON() ([]byte, error) {
|
||||||
|
data := map[string]interface{}{}
|
||||||
|
for k, v := range r.Values {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
if r.ID != "" {
|
||||||
|
data["id"] = r.ID
|
||||||
|
}
|
||||||
|
data["type"] = r.Type
|
||||||
|
data["links"] = r.Links
|
||||||
|
if r.ActionLinks {
|
||||||
|
data["actionLinks"] = r.Actions
|
||||||
|
} else {
|
||||||
|
data["action"] = r.Actions
|
||||||
|
}
|
||||||
|
return json.Marshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionHandler func(actionName string, action *Action, request *APIContext) error
|
||||||
|
|
||||||
|
type RequestHandler func(request *APIContext) error
|
||||||
|
|
||||||
|
type Validator func(request *APIContext, data map[string]interface{}) error
|
||||||
|
|
||||||
|
type Formatter func(request *APIContext, resource *RawResource)
|
||||||
|
|
||||||
|
type ErrorHandler func(request *APIContext, err error)
|
||||||
|
|
||||||
|
type ResponseWriter interface {
|
||||||
|
Write(apiContext *APIContext, code int, obj interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessControl interface {
|
||||||
|
CanCreate(schema *Schema) bool
|
||||||
|
CanList(schema *Schema) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIContext struct {
|
||||||
|
Action string
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
Link string
|
||||||
|
Method string
|
||||||
|
Schema *Schema
|
||||||
|
Schemas *Schemas
|
||||||
|
Version *APIVersion
|
||||||
|
ResponseFormat string
|
||||||
|
ReferenceValidator ReferenceValidator
|
||||||
|
ResponseWriter ResponseWriter
|
||||||
|
QueryOptions *QueryOptions
|
||||||
|
Body map[string]interface{}
|
||||||
|
URLBuilder URLBuilder
|
||||||
|
AccessControl AccessControl
|
||||||
|
SubContext map[string]interface{}
|
||||||
|
|
||||||
|
Request *http.Request
|
||||||
|
Response http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APIContext) WriteResponse(code int, obj interface{}) {
|
||||||
|
r.ResponseWriter.Write(r, code, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ASC = SortOrder("asc")
|
||||||
|
DESC = SortOrder("desc")
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryOptions struct {
|
||||||
|
Sort Sort
|
||||||
|
Pagination *Pagination
|
||||||
|
Conditions []*QueryCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferenceValidator interface {
|
||||||
|
Validate(resourceType, resourceID string) bool
|
||||||
|
Lookup(resourceType, resourceID string) *RawResource
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLBuilder interface {
|
||||||
|
Current() string
|
||||||
|
Collection(schema *Schema) string
|
||||||
|
ResourceLink(resource *RawResource) string
|
||||||
|
RelativeToRoot(path string) string
|
||||||
|
//Link(resource Resource, name string) string
|
||||||
|
//ReferenceLink(resource Resource) string
|
||||||
|
//ReferenceByIdLink(resourceType string, id string) string
|
||||||
|
Version(version string) string
|
||||||
|
ReverseSort(order SortOrder) string
|
||||||
|
SetSubContext(subContext string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
ByID(apiContext *APIContext, schema *Schema, id string) (map[string]interface{}, error)
|
||||||
|
List(apiContext *APIContext, schema *Schema, opt *QueryOptions) ([]map[string]interface{}, error)
|
||||||
|
Create(apiContext *APIContext, schema *Schema, data map[string]interface{}) (map[string]interface{}, error)
|
||||||
|
Update(apiContext *APIContext, schema *Schema, data map[string]interface{}, id string) (map[string]interface{}, error)
|
||||||
|
Delete(apiContext *APIContext, schema *Schema, id string) error
|
||||||
|
}
|
121
types/types.go
Normal file
121
types/types.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type Collection struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Links map[string]string `json:"links"`
|
||||||
|
CreateTypes map[string]string `json:"createTypes,omitempty"`
|
||||||
|
Actions map[string]string `json:"actions"`
|
||||||
|
Pagination *Pagination `json:"pagination,omitempty"`
|
||||||
|
Sort *Sort `json:"sort,omitempty"`
|
||||||
|
Filters map[string][]Condition `json:"filters,omitempty"`
|
||||||
|
ResourceType string `json:"resourceType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericCollection struct {
|
||||||
|
Collection
|
||||||
|
Data []interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceCollection struct {
|
||||||
|
Collection
|
||||||
|
Data []Resource `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortOrder string
|
||||||
|
|
||||||
|
type Sort struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Order SortOrder `json:"order,omitempty"`
|
||||||
|
Reverse string `json:"reverse,omitempty"`
|
||||||
|
Links map[string]string `json:"sortLinks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Condition struct {
|
||||||
|
Modifier string `json:"modifier,omitempty"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
Marker string `json:"marker,omitempty"`
|
||||||
|
First string `json:"first,omitempty"`
|
||||||
|
Previous string `json:"previous,omitempty"`
|
||||||
|
Next string `json:"next,omitempty"`
|
||||||
|
Limit *int64 `json:"limit,omitempty"`
|
||||||
|
Total *int64 `json:"total,omitempty"`
|
||||||
|
Partial bool `json:"partial,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Links map[string]string `json:"links"`
|
||||||
|
Actions map[string]string `json:"actions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIVersion struct {
|
||||||
|
Group string `json:"group,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
SubContexts map[string]bool `json:"subContext,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
CodeName string `json:"-"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Links map[string]string `json:"links"`
|
||||||
|
Version APIVersion `json:"version"`
|
||||||
|
PluralName string `json:"pluralName,omitempty"`
|
||||||
|
ResourceMethods []string `json:"resourceMethods,omitempty"`
|
||||||
|
ResourceFields map[string]Field `json:"resourceFields,omitempty"`
|
||||||
|
ResourceActions map[string]Action `json:"resourceActions,omitempty"`
|
||||||
|
CollectionMethods []string `json:"collectionMethods,omitempty"`
|
||||||
|
CollectionFields map[string]Field `json:"collectionFields,omitempty"`
|
||||||
|
CollectionActions map[string]Action `json:"collectionActions,omitempty"`
|
||||||
|
CollectionFilters map[string]Filter `json:"collectionFilters,omitempty"`
|
||||||
|
|
||||||
|
InternalSchema *Schema `json:"-"`
|
||||||
|
Mapper Mapper `json:"-"`
|
||||||
|
ActionHandler ActionHandler `json:"-"`
|
||||||
|
LinkHandler RequestHandler `json:"-"`
|
||||||
|
ListHandler RequestHandler `json:"-"`
|
||||||
|
CreateHandler RequestHandler `json:"-"`
|
||||||
|
DeleteHandler RequestHandler `json:"-"`
|
||||||
|
UpdateHandler RequestHandler `json:"-"`
|
||||||
|
Formatter Formatter `json:"-"`
|
||||||
|
ErrorHandler ErrorHandler `json:"-"`
|
||||||
|
Validator Validator `json:"-"`
|
||||||
|
Store Store `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Default interface{} `json:"default,omitempty"`
|
||||||
|
Nullable bool `json:"nullable,omitempty"`
|
||||||
|
Create bool `json:"create,omitempty"`
|
||||||
|
WriteOnly bool `json:"writeOnly,omitempty"`
|
||||||
|
Required bool `json:"required,omitempty"`
|
||||||
|
Update bool `json:"update,omitempty"`
|
||||||
|
MinLength *int64 `json:"minLength,omitempty"`
|
||||||
|
MaxLength *int64 `json:"maxLength,omitempty"`
|
||||||
|
Min *int64 `json:"min,omitempty"`
|
||||||
|
Max *int64 `json:"max,omitempty"`
|
||||||
|
Options []string `json:"options,omitempty"`
|
||||||
|
ValidChars string `json:"validChars,omitempty"`
|
||||||
|
InvalidChars string `json:"invalidChars,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
CodeName string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
Input string `json:"input,omitempty"`
|
||||||
|
Output string `json:"output,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
Modifiers []string `json:"modifiers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOpts struct {
|
||||||
|
Filters map[string]interface{}
|
||||||
|
}
|
212
urlbuilder/url.go
Normal file
212
urlbuilder/url.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package urlbuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/norman/name"
|
||||||
|
"github.com/rancher/norman/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_OVERRIDE_URL_HEADER = "X-API-request-url"
|
||||||
|
FORWARDED_HOST_HEADER = "X-Forwarded-Host"
|
||||||
|
FORWARDED_PROTO_HEADER = "X-Forwarded-Proto"
|
||||||
|
FORWARDED_PORT_HEADER = "X-Forwarded-Port"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(r *http.Request, version types.APIVersion, schemas *types.Schemas) (types.URLBuilder, error) {
|
||||||
|
requestUrl := parseRequestUrl(r)
|
||||||
|
responseUrlBase, err := parseResponseUrlBase(requestUrl, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := &urlBuilder{
|
||||||
|
schemas: schemas,
|
||||||
|
requestUrl: requestUrl,
|
||||||
|
responseUrlBase: responseUrlBase,
|
||||||
|
apiVersion: version,
|
||||||
|
query: r.URL.Query(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type urlBuilder struct {
|
||||||
|
schemas *types.Schemas
|
||||||
|
requestUrl string
|
||||||
|
responseUrlBase string
|
||||||
|
apiVersion types.APIVersion
|
||||||
|
subContext string
|
||||||
|
query url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) SetSubContext(subContext string) {
|
||||||
|
u.subContext = subContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) ResourceLink(resource *types.RawResource) string {
|
||||||
|
if resource.ID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.constructBasicUrl(resource.Schema.Version, resource.Schema.PluralName, resource.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) ReverseSort(order types.SortOrder) string {
|
||||||
|
newValues := url.Values{}
|
||||||
|
for k, v := range u.query {
|
||||||
|
newValues[k] = v
|
||||||
|
}
|
||||||
|
newValues.Del("order")
|
||||||
|
newValues.Del("marker")
|
||||||
|
if order == types.ASC {
|
||||||
|
newValues.Add("order", string(types.DESC))
|
||||||
|
} else {
|
||||||
|
newValues.Add("order", string(types.ASC))
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.requestUrl + "?" + newValues.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) Current() string {
|
||||||
|
return u.requestUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) RelativeToRoot(path string) string {
|
||||||
|
return u.responseUrlBase + path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) Collection(schema *types.Schema) string {
|
||||||
|
plural := u.getPluralName(schema)
|
||||||
|
return u.constructBasicUrl(schema.Version, plural)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) Version(version string) string {
|
||||||
|
return fmt.Sprintf("%s/%s", u.responseUrlBase, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) constructBasicUrl(version types.APIVersion, parts ...string) string {
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
|
||||||
|
buffer.WriteString(u.responseUrlBase)
|
||||||
|
if version.Path == "" {
|
||||||
|
buffer.WriteString(u.apiVersion.Path)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(version.Path)
|
||||||
|
}
|
||||||
|
buffer.WriteString(u.subContext)
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
buffer.WriteString("/")
|
||||||
|
buffer.WriteString(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *urlBuilder) getPluralName(schema *types.Schema) string {
|
||||||
|
if schema.PluralName == "" {
|
||||||
|
return strings.ToLower(name.GuessPluralName(schema.ID))
|
||||||
|
}
|
||||||
|
return strings.ToLower(schema.PluralName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs the request URL based off of standard headers in the request, falling back to the HttpServletRequest.getRequestURL()
|
||||||
|
// if the headers aren't available. Here is the ordered list of how we'll attempt to construct the URL:
|
||||||
|
// - x-api-request-url
|
||||||
|
// - x-forwarded-proto://x-forwarded-host:x-forwarded-port/HttpServletRequest.getRequestURI()
|
||||||
|
// - x-forwarded-proto://x-forwarded-host/HttpServletRequest.getRequestURI()
|
||||||
|
// - x-forwarded-proto://host:x-forwarded-port/HttpServletRequest.getRequestURI()
|
||||||
|
// - x-forwarded-proto://host/HttpServletRequest.getRequestURI() request.getRequestURL()
|
||||||
|
//
|
||||||
|
// Additional notes:
|
||||||
|
// - With x-api-request-url, the query string is passed, it will be dropped to match the other formats.
|
||||||
|
// - If the x-forwarded-host/host header has a port and x-forwarded-port has been passed, x-forwarded-port will be used.
|
||||||
|
func parseRequestUrl(r *http.Request) string {
|
||||||
|
// Get url from custom x-api-request-url header
|
||||||
|
requestUrl := getOverrideHeader(r, DEFAULT_OVERRIDE_URL_HEADER, "")
|
||||||
|
if requestUrl != "" {
|
||||||
|
return strings.SplitN(requestUrl, "?", 2)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get url from standard headers
|
||||||
|
requestUrl = getUrlFromStandardHeaders(r)
|
||||||
|
if requestUrl != "" {
|
||||||
|
return requestUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use incoming url
|
||||||
|
return fmt.Sprintf("http://%s%s", r.Host, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUrlFromStandardHeaders(r *http.Request) string {
|
||||||
|
xForwardedProto := getOverrideHeader(r, FORWARDED_PROTO_HEADER, "")
|
||||||
|
if xForwardedProto == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
host := getOverrideHeader(r, FORWARDED_HOST_HEADER, "")
|
||||||
|
if host == "" {
|
||||||
|
host = r.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
port := getOverrideHeader(r, FORWARDED_PORT_HEADER, "")
|
||||||
|
if port == "443" || port == "80" {
|
||||||
|
port = "" // Don't include default ports in url
|
||||||
|
}
|
||||||
|
|
||||||
|
if port != "" && strings.Contains(host, ":") {
|
||||||
|
// Have to strip the port that is in the host. Handle IPv6, which has this format: [::1]:8080
|
||||||
|
if (strings.HasPrefix(host, "[") && strings.Contains(host, "]:")) || !strings.HasPrefix(host, "[") {
|
||||||
|
host = host[0:strings.LastIndex(host, ":")]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if port != "" {
|
||||||
|
port = ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s://%s%s%s", xForwardedProto, host, port, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOverrideHeader(r *http.Request, header string, defaultValue string) string {
|
||||||
|
// Need to handle comma separated hosts in X-Forwarded-For
|
||||||
|
value := r.Header.Get(header)
|
||||||
|
if value != "" {
|
||||||
|
return strings.TrimSpace(strings.Split(value, ",")[0])
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponseUrlBase(requestUrl string, r *http.Request) (string, error) {
|
||||||
|
path := r.URL.Path
|
||||||
|
|
||||||
|
index := strings.LastIndex(requestUrl, path)
|
||||||
|
if index == -1 {
|
||||||
|
// Fallback, if we can't find path in requestUrl, then we just assume the base is the root of the web request
|
||||||
|
u, err := url.Parse(requestUrl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
buffer.WriteString(u.Scheme)
|
||||||
|
buffer.WriteString("://")
|
||||||
|
buffer.WriteString(u.Host)
|
||||||
|
return buffer.String(), nil
|
||||||
|
} else {
|
||||||
|
return requestUrl[0:index], nil
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user