diff --git a/src/app/backend/apihandler.go b/src/app/backend/apihandler.go index 1ae72dc11703..a02c14e80fc2 100644 --- a/src/app/backend/apihandler.go +++ b/src/app/backend/apihandler.go @@ -77,6 +77,11 @@ func CreateHttpApiHandler(client *client.Client, heapsterClient HeapsterClient, To(apiHandler.handleNameValidity). Reads(AppNameValiditySpec{}). Writes(AppNameValidity{})) + deployWs.Route( + deployWs.POST("/validate/imagereference"). + To(apiHandler.handleImageReferenceValidity). + Reads(ImageReferenceValiditySpec{}). + Writes(ImageReferenceValidity{})) deployWs.Route( deployWs.POST("/validate/protocol"). To(apiHandler.handleProtocolValidity). @@ -246,6 +251,22 @@ func (apiHandler *ApiHandler) handleNameValidity(request *restful.Request, respo response.WriteHeaderAndEntity(http.StatusCreated, validity) } +// Handles image reference validation API call. +func (ApiHandler *ApiHandler) handleImageReferenceValidity(request *restful.Request, response *restful.Response){ + spec := new(ImageReferenceValiditySpec) + if err := request.ReadEntity(spec); err != nil { + handleInternalError(response, err) + return + } + + validity, err := ValidateImageReference(spec) + if err != nil { + handleInternalError(response, err) + return + } + response.WriteHeaderAndEntity(http.StatusCreated, validity) +} + // Handles protocol validation API call. func (apiHandler *ApiHandler) handleProtocolValidity(request *restful.Request, response *restful.Response) { spec := new(ProtocolValiditySpec) diff --git a/src/app/backend/validateimagereference.go b/src/app/backend/validateimagereference.go new file mode 100644 index 000000000000..7c178bac75ed --- /dev/null +++ b/src/app/backend/validateimagereference.go @@ -0,0 +1,46 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + distreference "github.com/docker/distribution/reference" +) + +// Specification for image referecne validation request. +type ImageReferenceValiditySpec struct { + // Reference of the image + Reference string `json:"reference"` +} + +// Describe validity of the image reference. +type ImageReferenceValidity struct { + // True when the image reference is valid. + Valid bool `json:"valid"` + // Error reason when image reference is valid + Reason string `json:"reason"` +} + +// Validation image reference. +func ValidateImageReference(spec *ImageReferenceValiditySpec) (*ImageReferenceValidity, error) { + log.Printf("Validating %s as a image reference", spec.Reference) + + s := spec.Reference + _, err := distreference.ParseNamed(s) + if err != nil { + return &ImageReferenceValidity{Valid: false, Reason: err.Error()}, nil + } + return &ImageReferenceValidity{Valid: true}, nil +} diff --git a/src/app/externs/backendapi.js b/src/app/externs/backendapi.js index c9805e7be129..4622023702c5 100644 --- a/src/app/externs/backendapi.js +++ b/src/app/externs/backendapi.js @@ -283,6 +283,21 @@ backendApi.AppNameValiditySpec; */ backendApi.AppNameValidity; +/** + * @typedef {{ + * reference: string + * }} + */ +backendApi.ImageReferenceValiditySpec; + +/** + * @typedef {{ + * valid: boolean, + * reason: string + * }} + */ +backendApi.ImageReferenceValidity; + /** * @typedef {{ * protocols: !Array diff --git a/src/app/frontend/deploy/deploy_module.js b/src/app/frontend/deploy/deploy_module.js index 918929a49ae8..5f0a30ac107c 100644 --- a/src/app/frontend/deploy/deploy_module.js +++ b/src/app/frontend/deploy/deploy_module.js @@ -22,6 +22,7 @@ import portMappingsDirective from './portmappings_directive'; import environmentVariablesDirective from './environmentvariables_directive'; import uploadDirective from './upload_directive'; import uniqueNameDirective from './uniquename_directive'; +import validImageReferenceDirective from './validimagereference_directive'; import validProtocolDirective from './validprotocol_directive'; import helpSectionModule from './helpsection/helpsection_module'; @@ -44,6 +45,7 @@ export default angular .directive('deployFromSettings', deployFromSettingsDirective) .directive('deployFromFile', deployFromFileDirective) .directive('kdUniqueName', uniqueNameDirective) + .directive('kdValidImagereference', validImageReferenceDirective) .directive('kdValidProtocol', validProtocolDirective) .directive('kdFileReader', fileReaderDirective) .directive('kdUpload', uploadDirective) diff --git a/src/app/frontend/deploy/deployfromsettings.html b/src/app/frontend/deploy/deployfromsettings.html index cc154ff333b6..c793ac9ecaa8 100644 --- a/src/app/frontend/deploy/deployfromsettings.html +++ b/src/app/frontend/deploy/deployfromsettings.html @@ -54,9 +54,10 @@ - + Container image is required. + Container image is invalid format. diff --git a/src/app/frontend/deploy/validimagereference_directive.js b/src/app/frontend/deploy/validimagereference_directive.js new file mode 100644 index 000000000000..b3a1aa3f98df --- /dev/null +++ b/src/app/frontend/deploy/validimagereference_directive.js @@ -0,0 +1,67 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** The name of this directive. */ +export const validImageReferenceValidationKey = 'validImageReference'; + +/** + * Validates image reference + * + * @param {!angular.$resource} $resource + * @param {!angular.$q} $q + * @return {!angular.Directive} + * @ngInject + */ +export default function validImageReferenceDirective($resource, $q) { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attrs, ctrl) { + /** @type {!angular.NgModelController} */ + let ngModelController = ctrl; + + ngModelController.$asyncValidators[validImageReferenceValidationKey] = + (reference) => { return validate(reference, $resource, $q); }; + }, + }; +} + +/** + * @param {string} reference + * @param {!angular.$resource} resource + * @param {!angular.$q} q + */ +function validate(reference, resource, q) { + let deferred = q.defer(); + + /** @type {!angular.Resource} */ + let resourceClass = resource('api/v1/appdeployments/validate/imagereference'); + /** @type {!backendApi.ImageReferenceValiditySpec} */ + let spec = {reference: reference}; + resourceClass.save( + spec, + /** + * @param {!backendApi.ImageReferenceValidity} validity + */ + (validity) => { + if (validity.valid === true) { + deferred.resolve(); + } else { + deferred.reject(validity.reason); + } + }, + () => { deferred.reject(); }); + + return deferred.promise; +} diff --git a/src/test/backend/validateimagereference_test.go b/src/test/backend/validateimagereference_test.go new file mode 100644 index 000000000000..e4303cd888b2 --- /dev/null +++ b/src/test/backend/validateimagereference_test.go @@ -0,0 +1,102 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "testing" +) + +func TestValidateImageReference(t *testing.T) { + cases := []struct { + reference string + expected bool + }{ + { + "test", + true, + }, + { + "test:1", + true, + }, + { + "test:tag", + true, + }, + { + "private.registry:5000/test:1", + true, + }, + { + "private.registry:5000/test", + true, + }, + { + "private.registry:5000/namespace/test:1", + true, + }, + { + "private.registry:port/namespace/test:1", + false, + }, + { + "private.registry:5000/n/a/m/e/s/test:1", + true, + }, + { + "private.registry:5000/namespace/test:image:1", + false, + }, + { + ":", + false, + }, + { + "private.registry:5000/test:1@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + true, + }, + { + "Test", + false, + }, + { + "private.registry:5000/Test", + false, + }, + { + "private@registry:5000/test", + false, + }, + { + "", + false, + }, + { + "test image:1", + false, + }, + } + + for _, c := range cases { + spec := &ImageReferenceValiditySpec{ + Reference: c.reference, + } + validity, _ := ValidateImageReference(spec) + if validity.Valid != c.expected { + t.Errorf("Expected %#v validity to be %#v, but was %#v\n", + c.reference, c.expected, validity) + } + } +}