Introduce inital draft of hawkBit SDK (#1638)

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>
This commit is contained in:
Avgustin Marinov
2024-02-12 16:30:22 +02:00
committed by GitHub
parent 0a01a23a60
commit 3b6570bca6
20 changed files with 1410 additions and 1 deletions

View File

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

View File

@@ -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());
}
}

View File

@@ -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<>());
}
}
};
}
}

View File

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

View File

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

View File

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