Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@
See https://github.com/quarkiverse/quarkus-operator-sdk/blob/main/pom.xml#L15 for the Quarkus
Operator SDK extension version defined below ("version.quarkus-operator-sdk").
-->
<version.quarkus-platform>3.20.1</version.quarkus-platform>
<version.quarkus-operator-sdk>7.1.4</version.quarkus-operator-sdk>
<version.quarkus-platform>3.23.4</version.quarkus-platform>
<version.quarkus-operator-sdk>7.2.0</version.quarkus-operator-sdk>

<version.cyclonedx>9.1.0-patch</version.cyclonedx>
<version.lombok>1.18.32</version.lombok>
Expand Down
4 changes: 4 additions & 0 deletions service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
<artifactId>sbomer-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2023 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.jboss.sbomer.service.nextgen.core.events;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true,
property = "type")
@JsonSubTypes({ @Type(EventStatusChangeEvent.class), @Type(GenerationStatusChangeEvent.class),
@Type(ResolveRequestEvent.class) })
public interface Event {
/**
* The type (name) of the event.
*
* @return Type of the event
*/
String type();

/**
* Key that is used for scheduling and storage purposes.
*
* @see https://www.confluent.io/learn/kafka-message-key/#what-is-a-kafka-message-key
* @return A string being the key
*/
String key();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,21 @@

import org.jboss.sbomer.service.nextgen.core.dto.model.EventRecord;

import com.fasterxml.jackson.annotation.JsonTypeName;

/**
* An event fired after a particular {@link EventRecord} has been created.
*/
public record EventStatusChangeEvent(EventRecord event) {
@JsonTypeName(EventStatusChangeEvent.TYPE)
public record EventStatusChangeEvent(String type, String key, EventRecord event) implements Event {
public static final String TYPE = "event.status.change";

public EventStatusChangeEvent(String key, EventRecord event) {
this(TYPE, key, event);
}

public EventStatusChangeEvent(EventRecord event) {
this(TYPE, event.id(), event);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,21 @@

import org.jboss.sbomer.service.nextgen.core.dto.model.GenerationRecord;

import com.fasterxml.jackson.annotation.JsonTypeName;

/**
* Event fired after any status change of a generation.
*/
public record GenerationStatusChangeEvent(GenerationRecord generation) {
@JsonTypeName(GenerationStatusChangeEvent.TYPE)
public record GenerationStatusChangeEvent(String type, String key, GenerationRecord generation) implements Event {
public static final String TYPE = "generation.status.change";

public GenerationStatusChangeEvent(String key, GenerationRecord generation) {
this(TYPE, key, generation);
}

public GenerationStatusChangeEvent(GenerationRecord generation) {
this(TYPE, generation.id(), generation);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,21 @@

import org.jboss.sbomer.service.nextgen.core.dto.model.EventRecord;

import com.fasterxml.jackson.annotation.JsonTypeName;

/**
* Event sent after a request for resolution is received.
*/
public record ResolveRequestEvent(EventRecord event) {
@JsonTypeName(ResolveRequestEvent.TYPE)
public record ResolveRequestEvent(String type, String key, EventRecord event) implements Event {
public static final String TYPE = "resolve.request"; // TODO: Rename

public ResolveRequestEvent(String key, EventRecord event) {
this(TYPE, key, event);
}

public ResolveRequestEvent(EventRecord event) {
this(TYPE, event.id(), event);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import org.jboss.sbomer.core.utils.PaginationParameters;
import org.jboss.sbomer.service.nextgen.core.dto.model.ManifestRecord;
import org.jboss.sbomer.service.nextgen.service.EntityMapper;
import org.jboss.sbomer.service.nextgen.service.model.Event;
import org.jboss.sbomer.service.nextgen.service.model.Manifest;
import org.jboss.sbomer.service.nextgen.service.rest.RestUtils;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.sbomer.service.nextgen.core.events;
package org.jboss.sbomer.service.nextgen.workflow;

import org.jboss.sbomer.service.nextgen.core.dto.model.EventRecord;
import org.jboss.sbomer.service.nextgen.core.dto.model.GenerationRecord;
import java.util.List;

/**
* Event fired after a Generation is scheduled.
*/
public record GenerationScheduledEvent(EventRecord event, GenerationRecord generation) {
import jakarta.validation.constraints.NotEmpty;

public record WorkflowDefinition(@NotEmpty List<WorkflowSpec> workflows) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2023 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.jboss.sbomer.service.nextgen.workflow;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;

import org.jboss.sbomer.core.errors.ApplicationException;
import org.jboss.sbomer.core.features.sbom.utils.ObjectMapperProvider;
import org.jboss.sbomer.service.nextgen.core.events.Event;
import org.jboss.sbomer.service.nextgen.core.utils.ConfigUtils;
import org.jboss.sbomer.service.nextgen.core.utils.JacksonUtils;
import org.jboss.sbomer.service.nextgen.workflow.triggers.EventTrigger;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.quarkus.qute.CompletedStage;
import io.quarkus.qute.Engine;
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.ReflectionValueResolver;
import io.quarkus.qute.TemplateException;
import io.quarkus.qute.ValueResolver;
import io.quarkus.scheduler.Scheduled;
import io.quarkus.scheduler.Scheduled.ConcurrentExecution;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;

@ApplicationScoped
@Slf4j
public class WorkflowProcessor {

public static final String CONFIG_KEY = "workflows-config.yaml";

public static class JsonNodeResolver implements ValueResolver {
@Override
public boolean appliesTo(EvalContext context) {
return context.getBase() instanceof JsonNode;
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
JsonNode node = (JsonNode) context.getBase();
String name = context.getName();
if (node.has(name)) {
if (node.get(name).isTextual()) {
return CompletedStage.of(node.get(name).asText());
} else if (node.get(name).isNumber()) {
return CompletedStage.of(node.get(name).asLong());
} else if (node.get(name).isBoolean()) {
return CompletedStage.of(node.get(name).asBoolean());
} else {
return CompletedStage.of(node.get(name));
}
}
return null;
}

}

KubernetesClient kubernetesClient;
String sbomerReleaseName;

Engine engine = Engine.builder()
.addDefaults()
.addValueResolver(new ReflectionValueResolver())
.addValueResolver(new JsonNodeResolver())
// .strictRendering(false)
.build();

String template;
WorkflowDefinition config;

@Inject
public WorkflowProcessor(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
this.sbomerReleaseName = ConfigUtils.getRelease();
}

public String getTemplate() {
if (template == null) {
updateTemplate();
}

return template;
}

@Scheduled(every = "20s", delay = 10, delayUnit = TimeUnit.SECONDS, concurrentExecution = ConcurrentExecution.SKIP)
public void updateTemplate() {
this.template = getCmContent(cmName());
}

/**
* Returns the content of the ConfigMap particular key as a {@code String}.
*
* @return CM content
*/
private String getCmContent(String configMapName) {
ConfigMap configMap = kubernetesClient.configMaps().withName(configMapName).get();

if (configMap == null) {
log.debug("Could not find '{}' ConfigMap", configMapName);
return null;
}

if (configMap.getData() == null) {
log.debug("'{}' ConfigMap content is empty", configMapName);
return null;
}

if (configMap.getData().get(CONFIG_KEY) == null) {
log.debug("'{}' ConfigMap does not contain the '{}' key", configMapName, CONFIG_KEY);
return null;
}

return configMap.getData().get(CONFIG_KEY);
}

public List<WorkflowSpec> eval(Event event) {
log.debug("Received event to evaluate");
log.trace(event.toString());

log.debug("Evaluating workflow definition with received event...");

String evaluated;

try {
evaluated = engine.parse(getTemplate()).data(Map.of("event", event)).render();
} catch (TemplateException e) {

log.error("Unable to parse workflow definition", e);

return Collections.emptyList();
}

log.debug("Definition successfully evaluated");
log.trace(evaluated);

WorkflowDefinition definition;

try {
definition = JacksonUtils
.parse(WorkflowDefinition.class, (ObjectNode) ObjectMapperProvider.yaml().readTree(evaluated));
} catch (JsonProcessingException e) {
throw new ApplicationException("Unable to read evaluated workflow definition", e);
}

log.debug("Finding matching workflows for event...");

// Filter workflows to match only these that use:
//
// * event as the trigger
// * all conditions match
List<WorkflowSpec> matchedWorkflows = definition.workflows()
.stream()
.filter(
w -> w.triggers().stream().anyMatch(t -> t instanceof EventTrigger)
&& w.conditions().stream().allMatch(c -> c.isMet()))
.toList();

log.debug("Found {} matching workflow(s)", matchedWorkflows.size());

return matchedWorkflows;
}

/**
* Constructs the ConfigMap name for profiles based on the SBOMer release name.
*
* @return Name of the ConfigMap holding generation profiles.
*/
private String cmName() {
return sbomerReleaseName + "-workflow-config";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2023 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.jboss.sbomer.service.nextgen.workflow;

import java.util.List;

import org.jboss.sbomer.service.nextgen.workflow.actions.Action;
import org.jboss.sbomer.service.nextgen.workflow.conditions.Condition;
import org.jboss.sbomer.service.nextgen.workflow.triggers.Trigger;

public record WorkflowSpec(List<Trigger> triggers, List<Condition> conditions, List<Action> actions) {

}
Loading
Loading