diff --git a/deploy/resources.go b/deploy/resources.go index e54ad49821..6c00158afc 100644 --- a/deploy/resources.go +++ b/deploy/resources.go @@ -214,6 +214,13 @@ var assets = func() http.FileSystem { compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x53\x4d\x6f\x9b\x40\x14\xbc\xef\xaf\x18\x99\x4b\x22\xf9\xa3\xed\xd1\x3d\xd1\xc4\x56\x51\x23\x5b\x0a\x4e\xa3\x1c\x9f\xe1\x19\x9e\x02\xfb\xe8\xee\x12\xe2\x7f\x5f\x2d\xb6\x9b\x44\xbd\x66\x6f\x88\x61\x3e\x76\x86\x04\xb3\xcf\x3b\x26\xc1\x9d\x14\x6c\x3d\x97\x08\x8a\x50\x33\xd2\x8e\x8a\x9a\x91\xeb\x21\x0c\xe4\x18\x6b\xed\x6d\x49\x41\xd4\xe2\x2a\xcd\xd7\xd7\xe8\x6d\xc9\x0e\x6a\x19\xea\xd0\xaa\x63\x93\xa0\x50\x1b\x9c\xec\xfb\xa0\x0e\xcd\x89\x10\x54\x39\xe6\x96\x6d\xf0\x73\x20\x67\x1e\xd9\x37\xdb\x5d\x76\xb3\xc2\x41\x1a\x46\x29\xfe\xf4\x11\x97\x18\x24\xd4\x26\x41\xa8\xc5\x63\x50\xf7\x8c\x83\x3a\x50\x59\x4a\x14\xa6\x06\x62\x0f\xea\xda\x93\x0d\xc7\x15\xb9\x52\x6c\x85\x42\xbb\xa3\x93\xaa\x0e\xd0\xc1\xb2\xf3\xb5\x74\x73\x93\x60\x17\x63\xe4\xeb\x8b\x13\x7f\xa2\x1d\x35\x83\xe2\x49\xfb\x73\x86\x77\x71\xcf\xb7\x30\xc5\x6f\x76\x3e\x8a\x7c\x9b\x7f\x31\x09\xae\x22\x64\x72\x7e\x39\xb9\xfe\x8e\xa3\xf6\x68\xe9\x08\xab\x01\xbd\xe7\x77\xcc\xfc\x5a\x70\x17\x20\x16\x85\xb6\x5d\x23\x64\x0b\x7e\x8b\xf5\x4f\x61\x8e\xd1\x40\xe4\xd0\x7d\x20\xb1\xa0\x31\x06\xf4\xf0\x1e\x06\x0a\x26\x31\x09\xc6\x53\x87\xd0\x2d\x17\x8b\x61\x18\xe6\x34\xda\x9d\xab\xab\x16\x97\x74\x8b\xbb\xec\x66\xb5\xc9\x57\xb3\xd1\xb2\x49\xf0\x60\x1b\xf6\x1e\x8e\xff\xf4\xe2\xb8\xc4\xfe\x08\xea\xba\x46\x0a\xda\x37\x8c\x86\x86\x58\xdc\xd8\xce\x58\xba\x58\x0c\x4e\x82\xd8\x6a\x0a\x7f\x6e\xdd\x24\x1f\xda\x79\xbb\xae\x8b\x3d\xf1\x1f\x00\x6a\x41\x16\x93\x34\x47\x96\x4f\xf0\x23\xcd\xb3\x7c\x6a\x12\x3c\x66\xbb\x9f\xdb\x87\x1d\x1e\xd3\xfb\xfb\x74\xb3\xcb\x56\x39\xb6\xf7\xb8\xd9\x6e\x6e\xb3\x5d\xb6\xdd\xe4\xd8\xae\x91\x6e\x9e\xf0\x2b\xdb\xdc\x4e\xc1\x12\x6a\x76\xe0\xd7\xce\x45\xff\xea\x20\xf1\x22\xb9\x8c\x9d\x5e\x06\x74\x31\x10\xf7\x11\x9f\x7d\xc7\x85\x1c\xa4\x40\x43\xb6\xea\xa9\x62\x54\xfa\xc2\xce\xc6\x79\x74\xec\x5a\xf1\xb1\x4e\x0f\xb2\xa5\x49\xd0\x48\x2b\x61\x5c\x91\xff\x3f\x54\x94\xf9\xcc\x7f\xcb\x50\x27\xe7\x39\x2d\xf1\xf2\xd5\x3c\x8b\x2d\x97\xc8\xd9\xbd\x48\xc1\x69\x51\x68\x6f\x83\x69\x39\x50\x49\x81\x96\x06\xb0\xd4\xf2\x12\x05\xb5\xdc\xcc\x9e\x67\xda\xb1\xa3\xa0\xce\x00\x0d\xed\xb9\xf1\x11\x82\x58\xe5\x12\x93\x33\x68\x62\xfe\x06\x00\x00\xff\xff\xaf\x8c\x67\xdd\x0f\x04\x00\x00"), }, + "/operator.yaml": &vfsgen۰CompressedFileInfo{ + name: "operator.yaml", + modTime: time.Time{}, + uncompressedSize: 2101, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x54\xc1\x6e\xe3\x36\x10\xbd\xeb\x2b\x1e\xac\xcb\x2e\x10\xdb\x9b\x3d\xaa\x27\xd5\x71\xb0\x42\x53\xd9\xb0\xbc\x0d\xf6\x54\x4c\xa8\x91\x44\x84\x22\x55\x92\x8a\x56\x7f\x5f\x50\xb6\x13\x3b\xbb\x4d\x7b\x08\xca\x93\xa4\x99\x79\xf3\xde\xcc\x13\x63\xcc\xdf\xef\x44\x31\xee\xa4\x60\xed\xb8\x84\x37\xf0\x0d\x23\xed\x48\x34\x8c\xc2\x54\x7e\x20\xcb\xb8\x35\xbd\x2e\xc9\x4b\xa3\xf1\x21\x2d\x6e\x3f\xa2\xd7\x25\x5b\x18\xcd\x30\x16\xad\xb1\x1c\xc5\x10\x46\x7b\x2b\x1f\x7a\x6f\x2c\xd4\x01\x10\x54\x5b\xe6\x96\xb5\x77\x0b\xa0\x60\x9e\xd0\xf3\xcd\x3e\x5b\xad\x51\x49\xc5\x28\xa5\x3b\x14\x71\x89\x41\xfa\x26\x8a\xe1\x1b\xe9\x30\x18\xfb\x88\xca\x58\x50\x59\xca\xd0\x98\x14\xa4\xae\x8c\x6d\x0f\x34\x2c\xd7\x64\x4b\xa9\x6b\x08\xd3\x8d\x56\xd6\x8d\x87\x19\x34\x5b\xd7\xc8\x6e\x11\xc5\xd8\x07\x19\xc5\xed\x89\x89\x3b\xc0\x4e\x3d\xbd\xc1\x37\xd3\x1f\x35\x9c\xc9\x3d\x4e\xe1\x0a\x7f\xb0\x75\xa1\xc9\xe7\xc5\xa7\x28\xc6\x87\x90\x32\x3b\x06\x67\x1f\x7f\xc1\x68\x7a\xb4\x34\x42\x1b\x8f\xde\xf1\x19\x32\x7f\x17\xdc\x79\x48\x0d\x61\xda\x4e\x49\xd2\x82\x5f\x64\x3d\x77\x58\x60\x22\x10\x30\xcc\x83\x27\xa9\x41\x93\x0c\x98\xea\x3c\x0d\xe4\xa3\x38\x8a\x31\x9d\xc6\xfb\x2e\x59\x2e\x87\x61\x58\xd0\x44\x77\x61\x6c\xbd\x3c\xa9\x5b\xde\x65\xab\x75\x5e\xac\xe7\x13\xe5\x28\xc6\x57\xad\xd8\x39\x58\xfe\xab\x97\x96\x4b\x3c\x8c\xa0\xae\x53\x52\xd0\x83\x62\x28\x1a\xc2\xe2\xa6\xed\x4c\x4b\x97\x1a\x83\x95\x5e\xea\xfa\x0a\xee\xb8\xf5\x28\xbe\xd8\xce\xcb\xb8\x4e\xf4\xa4\xbb\x48\x30\x1a\xa4\x31\x4b\x0b\x64\xc5\x0c\xbf\xa6\x45\x56\x5c\x45\x31\xee\xb3\xfd\x97\xcd\xd7\x3d\xee\xd3\xdd\x2e\xcd\xf7\xd9\xba\xc0\x66\x87\xd5\x26\xbf\xc9\xf6\xd9\x26\x2f\xb0\xb9\x45\x9a\x7f\xc3\x6f\x59\x7e\x73\x05\x96\xbe\x61\x0b\xfe\xde\xd9\xc0\xdf\x58\xc8\x30\x48\x2e\xc3\x4e\x4f\x06\x3a\x11\x08\xfe\x08\xef\xae\x63\x21\x2b\x29\xa0\x48\xd7\x3d\xd5\x8c\xda\x3c\xb1\xd5\xc1\x1e\x1d\xdb\x56\xba\xb0\x4e\x07\xd2\x65\x14\x43\xc9\x56\xfa\xc9\x45\xee\x47\x51\xa1\xcd\x7b\xfe\x5b\x11\x75\xf2\x68\xa7\x24\x6c\xc0\x2d\x9f\xae\xa3\x47\xa9\xcb\x04\x37\xdc\x29\x33\x86\x9f\x23\x6a\xd9\x53\x49\x9e\x92\x08\xd0\xd4\x72\x02\x41\x2d\xab\xf9\xe3\xdc\x74\x6c\xc9\x1b\x1b\x01\x8a\x1e\x58\xb9\x90\x82\x80\x94\x60\x76\x4c\x9a\x4d\x9f\xa6\x97\x73\x6f\x04\x0b\x1a\xcd\xda\x27\x78\x46\x09\x93\x0a\x08\x96\x27\x2f\xb8\x04\xd7\x11\xe0\xbc\x25\xcf\xf5\x78\xc0\xf6\x63\xc7\x09\x76\x2c\x2c\x93\xe7\x10\x66\xc5\xc2\x1b\x7b\x08\xb7\xe4\x45\x73\x77\xc6\xe5\x0d\xca\x9e\xdb\x4e\x91\xe7\x63\xe5\x99\xca\x70\xd4\x05\xc8\x1b\x30\x87\xf3\x9f\x04\x86\xc4\x93\xc8\xe9\x99\xed\x93\x14\x9c\x0a\x61\x7a\xed\xf3\xb7\x3a\x84\xfb\x8b\x64\xb8\x42\x5e\x28\xcd\xff\x8d\x14\x20\x5b\xaa\x39\x41\x69\xc4\x23\xdb\x85\x34\xcb\x03\xc3\xe5\xb1\x24\xb9\x5e\x7c\x5a\x7c\x9a\xef\x56\x9f\xcf\x6a\x84\x69\x5b\xd2\x65\x72\xf6\x69\x7e\xea\xf1\x1a\x7a\xdb\x2b\xb5\x35\x4a\x8a\x31\x41\x56\xe5\xc6\x6f\x2d\xbb\x60\x9a\x97\x3c\xd6\x4f\xe7\x50\x2f\xb4\xef\xd3\xfd\xea\xcb\x9f\x79\xfa\xfb\xba\xd8\xa6\xab\xf5\x45\x0e\xf0\x44\xaa\xe7\x5b\x6b\xda\xe4\x55\x00\xa8\x24\xab\x72\xc7\xd5\x8f\x91\x63\x6c\x4b\xbe\x49\x9e\x37\xba\x08\xed\x5c\x47\x82\x7f\x4a\x63\xb3\x5d\xef\xd2\xfd\x66\x37\x31\xf9\x19\x89\xd7\x5e\x7e\x0d\xb0\xdd\xdc\xfc\x63\xed\xfb\x09\xb8\x48\x8d\xf1\x3c\xb6\x70\xd1\x91\x1a\x68\x74\xd3\x4d\x71\xf2\x00\x9e\x45\x5f\x41\xea\x92\x3b\xd6\x25\x6b\xaf\x46\x54\xd6\xb4\x6f\xce\xfe\xa4\xeb\x7f\xdd\xcc\xdf\x01\x00\x00\xff\xff\x61\x8c\x0a\x1b\x35\x08\x00\x00"), + }, "/platform-cr.yaml": &vfsgen۰CompressedFileInfo{ name: "platform-cr.yaml", modTime: time.Time{}, @@ -362,6 +369,7 @@ var assets = func() http.FileSystem { fs["/operator-role-olm.yaml"].(os.FileInfo), fs["/operator-role-openshift.yaml"].(os.FileInfo), fs["/operator-service-account.yaml"].(os.FileInfo), + fs["/operator.yaml"].(os.FileInfo), fs["/platform-cr.yaml"].(os.FileInfo), fs["/platform-integration-kit-groovy.yaml"].(os.FileInfo), fs["/platform-integration-kit-java.yaml"].(os.FileInfo), diff --git a/docs/modules/ROOT/pages/configuration/configmap-secret.adoc b/docs/modules/ROOT/pages/configuration/configmap-secret.adoc index 25a27a919d..1be9f30b70 100644 --- a/docs/modules/ROOT/pages/configuration/configmap-secret.adoc +++ b/docs/modules/ROOT/pages/configuration/configmap-secret.adoc @@ -7,7 +7,7 @@ For the sake of simplicity, consider the following integration: [source,groovy] .props.groovy ---- -from('timer:props?period=1s') +from('timer:props?period=1000') .log('{{my.message}}') ---- diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 5329c0dd8a..c9ed983ecc 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -334,6 +334,7 @@ func (o *runCmdOptions) syncIntegration(c client.Client, sources []string) error files = append(files, sources...) files = append(files, o.Resources...) files = append(files, o.PropertyFiles...) + files = append(files, o.OpenAPIs...) for _, s := range files { if !isRemoteHTTPFile(s) { diff --git a/pkg/trait/openapi.go b/pkg/trait/openapi.go index 10d782873c..43895d47a7 100644 --- a/pkg/trait/openapi.go +++ b/pkg/trait/openapi.go @@ -18,7 +18,6 @@ limitations under the License. package trait import ( - "errors" "fmt" "io/ioutil" "os" @@ -28,6 +27,10 @@ import ( "strings" "github.com/apache/camel-k/pkg/util" + "github.com/apache/camel-k/pkg/util/digest" + "github.com/pkg/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -103,67 +106,13 @@ func (t *openAPITrait) Apply(e *Environment) error { return fmt.Errorf("no name defined for the openapi resource: %v", resource) } - tmpDir = path.Join(tmpDir, strconv.Itoa(i)) - err := os.MkdirAll(tmpDir, os.ModePerm) - if err != nil { - return err - } - - content := []byte(resource.Content) - if resource.Compression { - content, err = gzip.UncompressBase64(content) - if err != nil { - return err - } - } - - in := path.Join(tmpDir, resource.Name) - out := path.Join(tmpDir, "openapi-dsl.xml") - - err = ioutil.WriteFile(in, content, 0644) - if err != nil { - return err - } - - project, err := t.generateMavenProject(e) - if err != nil { - return err - } - - mc := maven.NewContext(tmpDir, project) - mc.LocalRepository = e.Platform.Status.Build.Maven.LocalRepository - mc.Timeout = e.Platform.Status.Build.Maven.GetTimeout().Duration - mc.AddArgument("-Dopenapi.spec=" + in) - mc.AddArgument("-Ddsl.out=" + out) - - settings, err := kubernetes.ResolveValueSource(e.C, e.Client, e.Integration.Namespace, &e.Platform.Status.Build.Maven.Settings) - if err != nil { - return err - } - if settings != "" { - mc.SettingsContent = []byte(settings) - } - - err = maven.Run(mc) - if err != nil { - return err - } - - content, err = ioutil.ReadFile(out) - if err != nil { - return err - } - - if resource.Compression { - c, err := gzip.CompressBase64(content) - if err != nil { - return nil - } + generatedContentName := fmt.Sprintf("%s-openapi-%03d", e.Integration.Name, i) - content = c + // Generate configmap or reuse existing one + if err := t.generateOpenAPIConfigMap(e, resource, tmpDir, generatedContentName); err != nil { + return errors.Wrapf(err, "cannot generate configmap for openapi resource %s", resource.Name) } - generatedContentName := fmt.Sprintf("%s-openapi-%03d", e.Integration.Name, i) generatedSourceName := strings.TrimSuffix(resource.Name, filepath.Ext(resource.Name)) + ".xml" generatedSources := make([]v1.SourceSpec, 0, len(e.Integration.Status.GeneratedSources)) @@ -190,38 +139,141 @@ func (t *openAPITrait) Apply(e *Environment) error { Language: v1.LanguageXML, }) - // - // Store the generated rest xml in a separate config map in order - // not to pollute the integration with generated data - // - cm := corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: generatedContentName, - Namespace: e.Integration.Namespace, - Labels: map[string]string{ - "camel.apache.org/integration": e.Integration.Name, - }, - Annotations: map[string]string{ - "camel.apache.org/source.language": string(v1.LanguageXML), - "camel.apache.org/source.name": resource.Name, - "camel.apache.org/source.compression": strconv.FormatBool(resource.Compression), - "camel.apache.org/source.generated": "true", - "camel.apache.org/source.type": string(v1.ResourceTypeOpenAPI), - }, - }, - Data: map[string]string{ - "content": string(content), - }, - } - e.Integration.Status.GeneratedSources = generatedSources + } + + return nil +} + +func (t *openAPITrait) generateOpenAPIConfigMap(e *Environment, resource v1.ResourceSpec, tmpDir, generatedContentName string) error { + cm := corev1.ConfigMap{} + key := client.ObjectKey{ + Namespace: e.Integration.Namespace, + Name: generatedContentName, + } + err := t.Client.Get(t.Ctx, key, &cm) + if err != nil && k8serrors.IsNotFound(err) { + return t.createNewOpenAPIConfigMap(e, resource, tmpDir, generatedContentName) + } else if err != nil { + return err + } + + // ConfigMap already present, let's check if the source has not changed + foundDigest := cm.Annotations["camel.apache.org/source.digest"] + + // Compute the new digest + newDigest, err := digest.ComputeForResource(resource) + if err != nil { + return err + } + + if foundDigest == newDigest { + // ConfigMap already exists and matches the source + // Re-adding it to update its revision + cm.ResourceVersion = "" e.Resources.Add(&cm) + return nil + } + return t.createNewOpenAPIConfigMap(e, resource, tmpDir, generatedContentName) +} + +func (t *openAPITrait) createNewOpenAPIConfigMap(e *Environment, resource v1.ResourceSpec, tmpDir, generatedContentName string) error { + tmpDir = path.Join(tmpDir, generatedContentName) + err := os.MkdirAll(tmpDir, os.ModePerm) + if err != nil { + return err + } + + content := []byte(resource.Content) + if resource.Compression { + content, err = gzip.UncompressBase64(content) + if err != nil { + return err + } + } + + in := path.Join(tmpDir, resource.Name) + out := path.Join(tmpDir, "openapi-dsl.xml") + + err = ioutil.WriteFile(in, content, 0644) + if err != nil { + return err + } + + project, err := t.generateMavenProject(e) + if err != nil { + return err + } + + mc := maven.NewContext(tmpDir, project) + mc.LocalRepository = e.Platform.Status.Build.Maven.LocalRepository + mc.Timeout = e.Platform.Status.Build.Maven.GetTimeout().Duration + mc.AddArgument("-Dopenapi.spec=" + in) + mc.AddArgument("-Ddsl.out=" + out) + + settings, err := kubernetes.ResolveValueSource(e.C, e.Client, e.Integration.Namespace, &e.Platform.Status.Build.Maven.Settings) + if err != nil { + return err + } + if settings != "" { + mc.SettingsContent = []byte(settings) + } + + err = maven.Run(mc) + if err != nil { + return err + } + + content, err = ioutil.ReadFile(out) + if err != nil { + return err + } + + if resource.Compression { + c, err := gzip.CompressBase64(content) + if err != nil { + return nil + } + + content = c + } + + // Compute the input digest and store it along with the configmap + hash, err := digest.ComputeForResource(resource) + if err != nil { + return err + } + + // + // Store the generated rest xml in a separate config map in order + // not to pollute the integration with generated data + // + cm := corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedContentName, + Namespace: e.Integration.Namespace, + Labels: map[string]string{ + "camel.apache.org/integration": e.Integration.Name, + }, + Annotations: map[string]string{ + "camel.apache.org/source.language": string(v1.LanguageXML), + "camel.apache.org/source.name": resource.Name, + "camel.apache.org/source.compression": strconv.FormatBool(resource.Compression), + "camel.apache.org/source.generated": "true", + "camel.apache.org/source.type": string(v1.ResourceTypeOpenAPI), + "camel.apache.org/source.digest": hash, + }, + }, + Data: map[string]string{ + "content": string(content), + }, } + e.Resources.Add(&cm) return nil } diff --git a/pkg/trait/owner.go b/pkg/trait/owner.go index e7e358e8a4..84236b8f5e 100644 --- a/pkg/trait/owner.go +++ b/pkg/trait/owner.go @@ -54,7 +54,7 @@ func (t *ownerTrait) Configure(e *Environment) (bool, error) { return false, nil } - return e.IntegrationInPhase(v1.IntegrationPhaseDeploying, v1.IntegrationPhaseRunning), nil + return e.IntegrationInPhase(v1.IntegrationPhaseInitialization, v1.IntegrationPhaseDeploying, v1.IntegrationPhaseRunning), nil } func (t *ownerTrait) Apply(e *Environment) error { diff --git a/pkg/util/digest/digest.go b/pkg/util/digest/digest.go index 62ec3face7..53469f14f4 100644 --- a/pkg/util/digest/digest.go +++ b/pkg/util/digest/digest.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "encoding/base64" "sort" + "strconv" v1 "github.com/apache/camel-k/pkg/apis/camel/v1" "github.com/apache/camel-k/pkg/util" @@ -121,6 +122,41 @@ func ComputeForIntegrationKit(kit *v1.IntegrationKit) (string, error) { return digest, nil } +// ComputeForResource returns a digest for the specific resource +func ComputeForResource(res v1.ResourceSpec) (string, error) { + hash := sha256.New() + // Operator version is relevant + if _, err := hash.Write([]byte(defaults.Version)); err != nil { + return "", err + } + + if _, err := hash.Write([]byte(res.Content)); err != nil { + return "", err + } + if _, err := hash.Write([]byte(res.Name)); err != nil { + return "", err + } + if _, err := hash.Write([]byte(res.Type)); err != nil { + return "", err + } + if _, err := hash.Write([]byte(res.ContentKey)); err != nil { + return "", err + } + if _, err := hash.Write([]byte(res.ContentRef)); err != nil { + return "", err + } + if _, err := hash.Write([]byte(res.MountPath)); err != nil { + return "", err + } + if _, err := hash.Write([]byte(strconv.FormatBool(res.Compression))); err != nil { + return "", err + } + + // Add a letter at the beginning and use URL safe encoding + digest := "v" + base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) + return digest, nil +} + func sortedTraitSpecMapKeys(m map[string]v1.TraitSpec) []string { res := make([]string, len(m)) i := 0