diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa26325..1f01f84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
+## [0.5.0] — 2023-11-02
+
+### Changed
+
+- Improved and enhanced onboarding and configuration management experience
+
+### Fixed
+
+- Fixed the logic to get the users
+- Fixed the issue of downloading correct Docker image
+- Fixed the issue of container startup
+
+## [0.4.0] — 2023-03-24
+
+### Changed
+
+- Refactoring and UX improvements
+- Full support on Windows
+
## [0.3.0] - 2023-01-10
### Added
diff --git a/Makefile b/Makefile
index bbd8b17..2e6154e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
IMAGE?=localstack/localstack-docker-desktop
-TAG?=0.4.2
+TAG?=0.5.0
BUILDER=buildx-multi-arch
@@ -15,6 +15,16 @@ install-extension: build-extension ## Install the extension
update-extension: build-extension ## Update the extension
docker extension update $(IMAGE):$(TAG)
+debug: ## Start the extension in debug mode
+ docker extension dev debug $(IMAGE)
+
+hot-reload: ## Enable hot reloading
+ docker extension dev ui-source $(IMAGE) http://localhost:3000
+ cd ui/ && npm start
+
+stop-hot-realoading: ## Disable hot reloading
+ docker extension dev reset $(IMAGE)
+
prepare-buildx: ## Create buildx builder for multi-arch build, if not exists
docker buildx inspect $(BUILDER) || docker buildx create --name=$(BUILDER) --driver=docker-container --driver-opt=network=host
diff --git a/README.md b/README.md
index 54ce9f6..9edc471 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ LocalStack empowers developers to use over 75+ AWS services locally while helpin
You can install the LocalStack Extension for Docker Desktop via pulling our public Docker image from Docker Hub:
```bash
-docker extension install localstack/localstack-docker-desktop:0.3.1
+docker extension install localstack/localstack-docker-desktop:0.5.0
```
To setup the Docker Extension by building the image locally, you can run the following commands:
@@ -44,15 +44,18 @@ To contribute, check out our [issue tracker](https://github.com/localstack/local
2. Open the Developer Tools or create new features:
```bash
- $ docker extension dev debug localstack/localstack-docker-desktop
+ $ make debug
```
3. Start the Extension on Docker Desktop and enable hot-reload using the following command:
```bash
- $ npm start
- $ docker extension dev ui-source localstack/localstack-docker-desktop http://localhost:3000
+ $ make hot-reloading
```
+4. Disable hot reloading:
+ ```bash
+ $ make stop-hot-reloading
+ ```
## Releases
Please refer to [`CHANGELOG`](CHANGELOG.md) to see the complete list of changes for each release.
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index b045c78..ae22e55 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -18,7 +18,9 @@ export const App = (): ReactElement => {
return (
<>
- {showForm && }
+ {showForm &&
+
+ }
diff --git a/ui/src/components/Header/Controller.tsx b/ui/src/components/Header/Controller.tsx
index c85db5c..a19e1d0 100644
--- a/ui/src/components/Header/Controller.tsx
+++ b/ui/src/components/Header/Controller.tsx
@@ -25,10 +25,10 @@ import { ProgressButton } from '../Feedback';
const EXCLUDED_ERROR_TOAST = ['INFO', 'WARN', 'DEBUG'];
export const Controller = (): ReactElement => {
- const { runConfigs, isLoading, createConfig } = useRunConfigs();
+ const { configData, isLoading, setRunningConfig: setBackendRunningConfig, createConfig } = useRunConfigs();
const { data, mutate } = useLocalStack();
const { user, os, hasSkippedConfiguration } = useMountPoint();
- const [runningConfig, setRunningConfig] = useState('Default');
+ const [runningConfig, setRunningConfig] = useState(configData.runningConfig ?? DEFAULT_CONFIGURATION_ID);
const [downloadProps, setDownloadProps] = useState({ open: false, image: IMAGE });
const [isStarting, setIsStarting] = useState(false);
const [isStopping, setIsStopping] = useState(false);
@@ -38,14 +38,18 @@ export const Controller = (): ReactElement => {
const tooltipLabel = isUnhealthy ? 'Unhealthy' : 'Healthy';
useEffect(() => {
- if (!isLoading && (!runConfigs || !runConfigs.find(item => item.name === 'Default'))) {
+ if (!isLoading &&
+ (!configData?.configs || !configData.configs?.find(item => item.id === DEFAULT_CONFIGURATION_ID))) {
createConfig({
name: 'Default', id: DEFAULT_CONFIGURATION_ID, vars: [],
- },
- );
+ });
+ }
+ if (!isLoading) {
+ setRunningConfig(configData.runningConfig ?? DEFAULT_CONFIGURATION_ID);
}
}, [isLoading]);
+
const buildMountArg = () => {
let location = 'LOCALSTACK_VOLUME_DIR=/tmp/localstack/volume';
@@ -69,7 +73,7 @@ export const Controller = (): ReactElement => {
const corsArg = ['-e', `EXTRA_CORS_ALLOWED_ORIGINS=${CORS_ALLOW_DEFAULT}`];
- const addedArgs = runConfigs.find(config => config.name === runningConfig)
+ const addedArgs = configData.configs.find(config => config.id === runningConfig)
.vars.map(item => {
if (item.variable === 'EXTRA_CORS_ALLOWED_ORIGINS') { // prevent overriding variable
corsArg.slice(0, 0);
@@ -88,8 +92,8 @@ export const Controller = (): ReactElement => {
const start = async () => {
const images = await ddClient.docker.listImages() as [DockerImage];
- const isPro = runConfigs.find(config => config.name === runningConfig)
- .vars.some(item => item.variable === 'LOCALSTACK_API_KEY');
+ const isPro = configData.configs.find(config => config.id === runningConfig)
+ .vars.some(item => item.variable === 'LOCALSTACK_API_KEY' && item.value);
const haveCommunity = images.some(image => image.RepoTags?.at(0) === IMAGE);
if (!haveCommunity) {
@@ -180,11 +184,11 @@ export const Controller = (): ReactElement => {
setRunningConfig(target.value)}
+ onChange={({ target }) => setBackendRunningConfig(target.value)}
>
{
- runConfigs?.map(config => (
- {config.name}
+ configData?.configs?.map(config => (
+ {config.name}
))
}
diff --git a/ui/src/components/Views/Configs/ConfigPage.tsx b/ui/src/components/Views/Configs/ConfigPage.tsx
index c3a3068..01d9da0 100644
--- a/ui/src/components/Views/Configs/ConfigPage.tsx
+++ b/ui/src/components/Views/Configs/ConfigPage.tsx
@@ -18,7 +18,7 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
export const ConfigPage = (): ReactElement => {
- const { runConfigs, deleteConfig } = useRunConfigs();
+ const { configData, deleteConfig } = useRunConfigs();
const mountPoint = useMountPoint();
const [openModal, setOpenModal] = useState(false);
const [targetConfig, setTargetConfig] = useState(null);
@@ -89,7 +89,7 @@ export const ConfigPage = (): ReactElement => {
config.id !== DEFAULT_CONFIGURATION_ID)}
+ rows={configData?.configs?.filter(config => config.id !== DEFAULT_CONFIGURATION_ID) || []}
columns={columns}
getRowId={(row) => (row).id as string || uuid()}
disableSelectionOnClick
diff --git a/ui/src/components/Views/Configs/SettingsForm.tsx b/ui/src/components/Views/Configs/SettingsForm.tsx
index c610909..e2e1186 100644
--- a/ui/src/components/Views/Configs/SettingsForm.tsx
+++ b/ui/src/components/Views/Configs/SettingsForm.tsx
@@ -41,7 +41,7 @@ export const SettingsForm = ({ initialState }: MountPointFormProps): ReactElemen
const { setMountPointData, user, os } = useMountPoint();
const ddClient = useDDClient();
- const steps = ['Enable Docker Desktop option', 'Set mount point'];
+ const steps = ['Enable Docker Desktop option', 'Launching pro container', 'Set mount point'];
const handleNext = () => {
if (activeStep !== steps.length - 1) {
@@ -158,7 +158,7 @@ export const SettingsForm = ({ initialState }: MountPointFormProps): ReactElemen
)}
- {activeStep === 0 ?
+ {activeStep === 0 &&
<>
Make sure to have the option "Show Docker Extensions system containers" enabled.
@@ -171,7 +171,15 @@ export const SettingsForm = ({ initialState }: MountPointFormProps): ReactElemen
In the bottom-right corner, select Apply & Restart
>
- :
+ }
+ {
+ activeStep === 1 &&
+
+ In order to start the Pro container, add a configuration with the variable LOCALSTACK_API_KEY
+ set to your API key and select that configuration in the top right corner
+
+ }
+ {activeStep === 2 &&
<>
Default mount point settings
diff --git a/ui/src/components/Views/Configs/UpsertConfig.tsx b/ui/src/components/Views/Configs/UpsertConfig.tsx
index 4c46524..a35b884 100644
--- a/ui/src/components/Views/Configs/UpsertConfig.tsx
+++ b/ui/src/components/Views/Configs/UpsertConfig.tsx
@@ -20,6 +20,12 @@ import { RunConfig } from '../../../types';
const DEFAULT_COLUMN_WIDTH = 2000;
+const COMMON_CONFIGURATIONS = [
+ ['DEBUG', '0', 'Flag to increase log level and print more verbose logs'],
+ ['PERSISTENCE', '0', 'Enable persistence'],
+ ['LOCALSTACK_API_KEY', '', 'API key to activate LocalStack Pro.'],
+];
+
type Props = {
config?: RunConfig,
open: boolean,
@@ -42,7 +48,14 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement =>
const [newVar, setNewVar] = useState('');
const [newValue, setNewValue] = useState('');
const [configName, setConfigName] = useState(config?.name || '');
- const [newConfig, setNewConfig] = useState(config || { name: '', id: uuid(), vars: [] } as RunConfig);
+ const [newConfig, setNewConfig] = useState(config ||
+ {
+ name: '',
+ id: uuid(),
+ vars: COMMON_CONFIGURATIONS.map(
+ ([variable, value, description]) => ({ variable, value, id: uuid(), description }),
+ ),
+ } as RunConfig);
const classes = useStyles();
const handleAddButtonPress = () => {
@@ -105,34 +118,41 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement =>
variant='outlined'
label='Configuration Name'
value={configName}
+ required
onChange={(e) => setConfigName(e.target.value)}
/>
Environment Variables
+ Environment Variables
}
>
{newConfig?.vars.map(item => (
-
- updateConfigKey(item.id, e.target.value.toLocaleUpperCase())}
- value={item.variable}
- />
- updateConfigValue(item.id, e.target.value)}
- value={item.value} />
- handleRemoveButtonPress(item.id)} >
-
-
+
+
+ updateConfigKey(item.id, e.target.value.toLocaleUpperCase())}
+ value={item.variable}
+ />
+ updateConfigValue(item.id, e.target.value)}
+ value={item.value} />
+ handleRemoveButtonPress(item.id)} >
+
+
+
+ {item.description &&
+ {item.description}
+ }
))}
@@ -173,6 +193,7 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement =>
Save & Exit
diff --git a/ui/src/services/hooks/api.ts b/ui/src/services/hooks/api.ts
index 79bfd62..784ecd5 100644
--- a/ui/src/services/hooks/api.ts
+++ b/ui/src/services/hooks/api.ts
@@ -1,12 +1,13 @@
import useSWR from 'swr';
import { STORAGE_KEY_ENVVARS, STORAGE_KEY_LOCALSTACK, STORAGE_KEY_MOUNT } from '../../constants';
-import { DockerContainer, mountPointData, RunConfig } from '../../types';
+import { ConfigData, DockerContainer, mountPointData, RunConfig } from '../../types';
import { isALocalStackContainer, isJson } from '../util';
import { useDDClient } from './utils';
interface useRunConfigsReturn {
- runConfigs: RunConfig[],
+ configData: ConfigData,
isLoading: boolean,
+ setRunningConfig: (data: string) => unknown;
createConfig: (data: RunConfig) => unknown;
updateConfig: (data: RunConfig) => unknown;
deleteConfig: (data: string) => unknown;
@@ -16,6 +17,17 @@ interface HTTPMessageBody {
Message: string,
}
+const adaptVersionData = (data: HTTPMessageBody, error: Error) => {
+ const newData = (!data || !data?.Message || error) ?
+ { configs: [], runningConfig: null }
+ :
+ JSON.parse(data?.Message);
+ if (Array.isArray(newData)) {
+ return { configs: newData, runningConfig: newData.at(0).id ?? null };
+ }
+ return newData;
+};
+
export const useRunConfigs = (): useRunConfigsReturn => {
const cacheKey = STORAGE_KEY_ENVVARS;
const ddClient = useDDClient();
@@ -29,6 +41,11 @@ export const useRunConfigs = (): useRunConfigsReturn => {
mutate();
};
+ const setRunningConfig = async (configId: string) => {
+ await ddClient.extension.vm.service.put('/configs/running', { Data: JSON.stringify(configId) });
+ mutate();
+ };
+
const createConfig = async (newData: RunConfig) => {
await ddClient.extension.vm.service.post('/configs', { Data: JSON.stringify(newData) });
mutate();
@@ -39,9 +56,11 @@ export const useRunConfigs = (): useRunConfigsReturn => {
mutate();
};
+
return {
- runConfigs: (!data || !data?.Message || error) ? [] : JSON.parse(data?.Message),
+ configData: adaptVersionData(data, error),
isLoading: isValidating || (!error && !data),
+ setRunningConfig,
createConfig,
updateConfig,
deleteConfig,
@@ -78,7 +97,7 @@ export const useMountPoint = (): useMountPointReturn => {
return {
user: mountPointData?.user,
os: mountPointData?.os,
- showForm: mountPointData?.showForm == null? true : mountPointData?.showForm,
+ showForm: mountPointData?.showForm == null ? true : mountPointData?.showForm,
showSetupWarning: mountPointData?.showSetupWarning == null ? true : mountPointData?.showSetupWarning,
hasSkippedConfiguration: mountPointData?.hasSkippedConfiguration || false,
isLoading: isValidating || (!error && !data),
@@ -105,7 +124,7 @@ export const useLocalStack = (): useLocalStackReturn => {
/*
* compares whether the old (b) status aligns with that of new (a) status
*/
- (a, b) => a?.Id === b?.Id && a?.Status.includes('unhealthy') === b?.Status.includes('unhealthy'),
+ (a, b) => a?.Id === b?.Id && a?.Status.includes('unhealthy') === b?.Status.includes('unhealthy'),
},
);
diff --git a/ui/src/services/util/containers.ts b/ui/src/services/util/containers.ts
index 96c3ff4..922bd4e 100644
--- a/ui/src/services/util/containers.ts
+++ b/ui/src/services/util/containers.ts
@@ -1,3 +1,4 @@
+import { PRO_IMAGE } from '../../constants';
import { DockerContainer, DockerImage } from '../../types';
/**
@@ -10,9 +11,23 @@ export function removeRepoFromImage(repoTag: string) {
return repoTag.split('/').at(-1);
}
+const removeDockerTag = (imageString:string ) => {
+ // Split the image string by ":" to check if it has a tag
+ const parts = imageString.split(':');
+
+ // If there is no tag, return the original string as is
+ if (parts.length === 1) {
+ return imageString;
+ }
+
+ // If there is a tag, return the part before the last ":" (image without tag)
+ return parts.slice(0, -1).join(':');
+};
export function removeTagFromImage(image: DockerImage){
- return image.RepoTags?.at(0)?.split(':').slice(0, -1).join(':');
+ return removeDockerTag(image.RepoTags?.at(0));
}
-export const isALocalStackContainer = (container: DockerContainer) =>
- (container.Image === 'localstack/localstack' || container.Image === 'localstack/localstack-pro');
+export const isALocalStackContainer = (container: DockerContainer) => {
+ const image = removeDockerTag(container.Image);
+ return image === 'localstack/localstack' || image === PRO_IMAGE;
+};
diff --git a/ui/src/types/global.ts b/ui/src/types/global.ts
index 079e156..2850be4 100644
--- a/ui/src/types/global.ts
+++ b/ui/src/types/global.ts
@@ -5,6 +5,7 @@ interface envVar {
variable: string,
value: string,
id: string;
+ description?: string;
}
export interface RunConfig {
@@ -13,6 +14,11 @@ export interface RunConfig {
vars: Optional,
}
+export interface ConfigData {
+ runningConfig: string,
+ configs: RunConfig[],
+}
+
export interface mountPointData {
user: string,
os: string,
diff --git a/vm/main.go b/vm/main.go
index 3ba8ee7..544b367 100644
--- a/vm/main.go
+++ b/vm/main.go
@@ -41,6 +41,7 @@ func main() {
router.GET("/configs", getSettings)
router.POST("/configs", createSetting)
router.PUT("/configs", updateSetting)
+ router.PUT("/configs/running", updateRunningConfig)
router.DELETE("/configs/:id", deleteSetting)
router.GET("/mount", getMount)
router.POST("/mount", setMount)
@@ -67,10 +68,49 @@ type Configuration struct {
} `json:"vars"`
}
+type ConfigurationData struct {
+ RunningConfig string `json:"runningConfig"`
+ Configs []Configuration `json:"configs"`
+}
+
type Payload struct {
Data string `json:"data"`
}
+func convertConfigurationVersion(loadedData []byte) ([]Configuration, error) {
+ var parsedContent []Configuration
+ err := json.Unmarshal(loadedData, &parsedContent)
+ return parsedContent, err
+}
+
+func updateRunningConfig(ctx echo.Context) error {
+ var payload Payload
+ var reqContent string
+ var parsedContent ConfigurationData
+
+ ctx.Bind(&payload)
+ json.Unmarshal([]byte(payload.Data), &reqContent)
+ savedData, file, _ := readDataKeepOpen()
+ defer file.Close()
+
+ err := json.Unmarshal(savedData, &parsedContent)
+
+ if err != nil {
+ configuration, newErr := convertConfigurationVersion(savedData)
+ if newErr != nil {
+ return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]})
+ }
+ parsedContent.Configs = configuration
+ }
+
+ parsedContent.RunningConfig = reqContent
+ err = writeData(parsedContent, file)
+ if err == nil {
+ return ctx.NoContent(http.StatusOK)
+ }
+ return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[1]})
+}
+
func getMount(ctx echo.Context) error {
content, err := readData(MOUNT_POINT_FILE)
if err != nil {
@@ -108,7 +148,7 @@ func getSettings(ctx echo.Context) error {
func updateSetting(ctx echo.Context) error {
var payload Payload
var reqContent Configuration
- var parsedContent []Configuration
+ var parsedContent ConfigurationData
var indexToChange int = -1
ctx.Bind(&payload)
@@ -119,21 +159,26 @@ func updateSetting(ctx echo.Context) error {
err := json.Unmarshal(savedData, &parsedContent)
if err != nil {
- return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]})
+ configuration, newErr := convertConfigurationVersion(savedData)
+ if newErr != nil {
+ return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]})
+ }
+ parsedContent.Configs = configuration
}
- for index, item := range parsedContent {
+ for index, item := range parsedContent.Configs {
if item.ID == reqContent.ID {
indexToChange = index
}
}
if indexToChange == -1 { //no config with that id found
- parsedContent = append(parsedContent, reqContent)
+ parsedContent.Configs = append(parsedContent.Configs, reqContent)
} else {
- parsedContent[indexToChange] = reqContent
+ parsedContent.Configs[indexToChange] = reqContent
}
+ parsedContent.RunningConfig = reqContent.ID
err = writeData(parsedContent, file)
if err == nil {
return ctx.NoContent(http.StatusOK)
@@ -144,7 +189,7 @@ func updateSetting(ctx echo.Context) error {
func deleteSetting(ctx echo.Context) error {
var idToDelete string
- var parsedContent []Configuration
+ var parsedContent ConfigurationData
var indexToDelete int = -1
idToDelete = ctx.Param("id")
@@ -156,17 +201,18 @@ func deleteSetting(ctx echo.Context) error {
return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]})
}
- for index, item := range parsedContent {
+ for index, item := range parsedContent.Configs {
if item.ID == idToDelete {
indexToDelete = index
}
}
if indexToDelete != -1 {
- if indexToDelete != len(parsedContent)-1 {
- parsedContent[indexToDelete] = parsedContent[len(parsedContent)-1]
+ if indexToDelete != len(parsedContent.Configs)-1 {
+ parsedContent.Configs[indexToDelete] = parsedContent.Configs[len(parsedContent.Configs)-1]
}
- parsedContent = parsedContent[:len(parsedContent)-1]
+ parsedContent.Configs = parsedContent.Configs[:len(parsedContent.Configs)-1]
+ parsedContent.RunningConfig = "00000000-0000-0000-0000-000000000000"
}
err = writeData(parsedContent, file)
@@ -182,7 +228,7 @@ func createSetting(ctx echo.Context) error {
var payload Payload
var reqContent Configuration
- var parsedContent []Configuration
+ var parsedContent ConfigurationData
ctx.Bind(&payload)
json.Unmarshal([]byte(payload.Data), &reqContent)
@@ -191,11 +237,17 @@ func createSetting(ctx echo.Context) error {
err := json.Unmarshal(savedData, &parsedContent)
if err != nil {
- return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]})
+ configuration, newErr := convertConfigurationVersion(savedData)
+ if newErr != nil {
+ return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[0]})
+ }
+ parsedContent.Configs = configuration
}
- updatedArray := append(parsedContent, reqContent)
- err = writeData(updatedArray, file)
+ updatedArray := append(parsedContent.Configs, reqContent)
+ parsedContent.Configs = updatedArray
+ parsedContent.RunningConfig = reqContent.ID
+ err = writeData(parsedContent, file)
if err != nil {
return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: ERRORS[1]})
@@ -241,7 +293,7 @@ func readDataKeepOpen() ([]byte, *os.File, error) {
return content, file, err
}
-func writeData(data []Configuration, file *os.File) error {
+func writeData(data ConfigurationData, file *os.File) error {
jsonData, err := json.Marshal(data)
if err == nil {
file.Truncate(0)