1
0
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:
Darren Shepherd
2017-11-10 21:44:02 -07:00
parent c06696a1e4
commit c8cab3f4f8
72 changed files with 5674 additions and 1211 deletions

View File

@@ -2,3 +2,4 @@
./.dapper ./.dapper
./dist ./dist
./.trash-cache ./.trash-cache
./.idea

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.idea
/.dapper /.dapper
/bin /bin
/dist /dist

8
.idea/modules.xml generated
View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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="&lt;template&gt;" 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="&lt;template&gt;" 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>

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
package api
import "github.com/rancher/norman/types"
type ResponseWriter interface {
Write(apiContext *types.APIContext, code int, obj interface{})
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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)
}

View 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
}
`

View 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
View 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
View 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
View 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}}`

View File

@@ -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
}

View File

@@ -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
View 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
View File

@@ -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
View 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)
}

View File

@@ -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, "*/*")
} }

View File

@@ -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
View 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
View 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
}

View File

@@ -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
View 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))
}

View File

@@ -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,
}
}

View File

@@ -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
View 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
View 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:]
}

View 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
View 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
}

View File

@@ -1,7 +0,0 @@
package store
type ReferenceValidator interface {
Validate(resourceType, resourceID string) bool
}

View 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
View 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
View 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)
}

View 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}
}
}

View 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
View 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
View 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",
// },
// },
// },
//}
)

View 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
}

View 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, "/")...)
}

View 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
}

View 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
}

View 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
}

View 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),
// }
//}

View 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
}

View 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
}

View 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)
}

View File

@@ -0,0 +1 @@
package mapper

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
}
}