Skip to content

Commit

Permalink
Introduce inital draft of hawkBit SDK (#1638)
Browse files Browse the repository at this point in the history
Intends to provide a Java SDK facilitating:
* development of back-end integrations using mgmt api (including UI-s)
* development of java based high-end devices (which could run Spring apps) to communicate with hawkBit via DDI API
* implementation of demo/test cases using device & management SDK

Status: initial draft
 - Feign client did & management API - done
 - Hal/HATEAOS Support - works (including in non-web apps)
 - device communication works when no software updates (e.g. pulling software base)
 - demo for single and multiple devices simulation (including management API uses)
 - TODO - fix software update flows
 - TODO - provide more integration points for developers to interact with device SDK

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
  • Loading branch information
avgustinmm committed Feb 12, 2024
1 parent 0a01a23 commit 3b6570b
Show file tree
Hide file tree
Showing 20 changed files with 1,410 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ maven.properties

# Test Files
*.tmp
spring-shell.log

# Documentation
.gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ public static MgmtSystemTenantConfigurationValue toResponseDefaultDsType(Long de
.withSelfRel().expand());
return restConfValue;
}
}
}
48 changes: 48 additions & 0 deletions hawkbit-sdk/hawkbit-sdk-commons/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
Copyright (c) 2023 Bosch.IO GmbH and others
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
-->
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-sdk</artifactId>
<version>${revision}</version>
</parent>

<artifactId>hawkbit-sdk-commons</artifactId>
<name>hawkBit :: SDK :: Commons</name>
<description>SDK commons</description>

<properties>
<spring-cloud-starter-openfeign.version>4.0.4</spring-cloud-starter-openfeign.version>
<openfeign-hc5.version>13.0</openfeign-hc5.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud-starter-openfeign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>${openfeign-hc5.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.sdk;

import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

@Data
@Builder
public class Controller {

// id of the tenant
@NonNull
private String controllerId;
// (target) security token
@Nullable
private String securityToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.sdk;

import feign.Client;
import feign.Contract;
import feign.Feign;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;

@Slf4j
@Builder
public class HawkbitClient {

private static final String AUTHORIZATION = "Authorization";
private static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();

private final HawkbitServer hawkBitServerProperties;

private final Client client;
private final Encoder encoder;
private final Decoder decoder;
private final Contract contract;

public HawkbitClient(
final HawkbitServer hawkBitServerProperties,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
this.hawkBitServerProperties = hawkBitServerProperties;
this.client = client;
this.encoder = encoder;
this.decoder = decoder;
this.contract = contract;
}

public <T> T mgmtService(final Class<T> serviceType, final Tenant tenantProperties) {
return service(serviceType, tenantProperties, null);
}
public <T> T ddiService(final Class<T> serviceType, final Tenant tenantProperties, final Controller controller) {
return service(serviceType, tenantProperties, controller);
}

private <T> T service(final Class<T> serviceType, final Tenant tenantProperties, final Controller controller) {
return Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.errorDecoder((methodKey, response) -> {
final Exception e = DEFAULT_ERROR_DECODER.decode(methodKey, response);
log.trace("REST API call failed!", e);
return e;
})
.contract(contract)
.requestInterceptor(controller == null ?
template -> {
template.header(AUTHORIZATION,
"Basic " +
Base64.getEncoder()
.encodeToString(
(Objects.requireNonNull(tenantProperties.getUsername(),
"User is null!") +
":" +
Objects.requireNonNull(tenantProperties.getPassword(),
"Password is not available!"))
.getBytes(StandardCharsets.ISO_8859_1)));
} :
template -> {
if (ObjectUtils.isEmpty(tenantProperties.getGatewayToken())) {
if (!ObjectUtils.isEmpty(controller.getSecurityToken())) {
template.header(AUTHORIZATION, "TargetToken " + controller.getSecurityToken());
} // else do not sent authentication
} else {
template.header(AUTHORIZATION, "GatewayToken " + tenantProperties.getGatewayToken());
}
})
.target(serviceType,
controller == null ?
hawkBitServerProperties.getMgmtUrl() :
hawkBitServerProperties.getDdiUrl());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.sdk;

import feign.Contract;
import feign.MethodMetadata;
import feign.RequestInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.hateoas.WebConvertersCustomizer;
import org.springframework.cloud.openfeign.support.HttpMessageConverterCustomizer;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.WebConverters;
import org.springframework.http.MediaType;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;

@Slf4j
@Configuration
@EnableConfigurationProperties({ HawkbitServer.class, Tenant.class})
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
@Import(FeignClientsConfiguration.class)
public class HawkbitSDKConfigurtion {

/**
* An feign request interceptor to set the defined {@code Accept} and {@code Content-Type} headers for each request
* to {@code application/json}.
*
* TODO - is this needed?
*/
@Bean
@Primary
public RequestInterceptor jsonHeaderInterceptorOverride() {
return template -> template
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE);
}

// takes place only when spring app is started in non-web-app mode
// in that case org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration
// is explicitly disabled and HAL/HATEOAS support doesn't work
@Bean
@ConditionalOnMissingBean
@ConditionalOnNotWebApplication
@ConditionalOnClass({ WebConverters.class})
public HttpMessageConverterCustomizer webConvertersCustomizerOverrider(WebConverters webConverters) {
return new WebConvertersCustomizer(webConverters);
}
// another option would be something like (need to import io.github.openfeign:feign-jackson
// @Bean @Primary @ConditionalOnNotWebApplication
// public Decoder feignDecoderOverride() {
// return new ResponseEntityDecoder(new JacksonDecoder(new ObjectMapper().registerModule(new Jackson2HalModule())));
// }

/**
* Own implementation of the {@link SpringMvcContract} which catches the {@link IllegalStateException} which occurs
* due multiple produces and consumes values in the request-mapping
* annotation.https://github.com/spring-cloud/spring-cloud-netflix/issues/808
*
* TODO - is this needed?
*/
@Bean
@Primary
public Contract feignContractOverride() {
return new SpringMvcContract() {

@Override
protected void processAnnotationOnMethod(final MethodMetadata data, final Annotation methodAnnotation, final Method method) {
try {
super.processAnnotationOnMethod(data, methodAnnotation, method);
} catch (final IllegalStateException e) {
// ignore illegalstateexception here because it's thrown because of
// multiple consumers and produces, see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/808
log.trace(e.getMessage(), e);

// This line from super is mandatory to avoid that access to the
// expander causes a nullpointer.
data.indexToExpander(new LinkedHashMap<>());
}
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.sdk;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.lang.NonNull;

@ConfigurationProperties(prefix="hawkbit.server")
@Data
public class HawkbitServer {

@NonNull
private String mgmtUrl = "http://localhost:8080";
@NonNull
private String ddiUrl = "http://localhost:8081";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.sdk;

import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

@ConfigurationProperties("hawkbit.tenant")
@Data
public class Tenant {

// id of the tenant
@NonNull
private String tenantId = "DEFAULT";

// basic auth user, to access management api
@Nullable
private String username = "admin";
@ToString.Exclude
@Nullable
private String password = "admin";

// gateway token
@Nullable
private String gatewayToken;

private boolean downloadAuthenticationEnabled;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#
# Copyright (c) 2023 Bosch.IO GmbH and others
#
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#

spring.cloud.openfeign.httpclient.hc5.enabled=true

Loading

0 comments on commit 3b6570b

Please sign in to comment.