Skip to content

Commit

Permalink
[JENKINS-69853] User experimental flags (jenkinsci#7299)
Browse files Browse the repository at this point in the history
* [JENKINS-69853] User experimental flags

* Applying feedbacks from Tim

* Improve default wording + padding + title

* Remove the "UI"

* Correct Spotbugs

---------

Co-authored-by: Alexander Brandes <[email protected]>
Co-authored-by: Tim Jacomb <[email protected]>
  • Loading branch information
3 people authored Mar 12, 2023
1 parent 6c076b9 commit af09d67
Show file tree
Hide file tree
Showing 14 changed files with 963 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* The MIT License
*
* Copyright (c) 2022, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.model.experimentalflags;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
* @since TODO
*/
public abstract class BooleanUserExperimentalFlag extends UserExperimentalFlag<Boolean> {
protected BooleanUserExperimentalFlag(@NonNull String flagKey) {
super(flagKey);
}

@Override
public @NonNull Boolean getDefaultValue() {
return false;
}

@Override
public Object serializeValue(Boolean rawValue) {
if (rawValue == null) {
return null;
}
return rawValue ? "true" : "false";
}

@Override
protected Boolean deserializeValue(Object serializedValue) {
if (serializedValue.equals("true")) {
return Boolean.TRUE;
}
if (serializedValue.equals("false")) {
return Boolean.FALSE;
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* The MIT License
*
* Copyright (c) 2022, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.model.experimentalflags;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.User;

/**
* User specific experimental flag to enable or disable specific behavior.
* As it's user specific, usually this kind of feature flag is only used for UI.
*
* @since TODO
*/
public abstract class UserExperimentalFlag<T> implements ExtensionPoint {
private final String flagKey;

protected UserExperimentalFlag(@NonNull String flagKey) {
this.flagKey = flagKey;
}

public abstract @NonNull T getDefaultValue();

/**
* Convert the usable value into a serializable form that can be stored in the user property.
* If no changes are necessary, simply returning the {@code rawValue} is fine.
*/
public abstract @Nullable Object serializeValue(T rawValue);

/**
* Convert the serialized value into the usable instance.
* If the instance is invalid (like after migration),
* returning {@code null} will force to return the {@link #getDefaultValue()}
*/
protected abstract @Nullable T deserializeValue(Object serializedValue);

/**
* The name that will be used in the configuration page for that flag
* It must be user readable
*/
public abstract String getDisplayName();

/**
* Describe what the flag is changing depending on its value.
* This method is called in description.jelly, which could be overloaded by children.
* It could return HTML content.
*/
public abstract @Nullable String getShortDescription();

/**
* The ID used by the machine to link the flag with its value within the user properties
*/
public @NonNull String getFlagKey() {
return flagKey;
}

public @NonNull T getFlagValue() {
User currentUser = User.current();
if (currentUser == null) {
// the anonymous user is not expected to use flags
return this.getDefaultValue();
}
return this.getFlagValue(currentUser);
}

public @NonNull T getFlagValue(User user) {
UserExperimentalFlagsProperty property = user.getProperty(UserExperimentalFlagsProperty.class);
if (property == null) {
// if for whatever reason there is no such property
return this.getDefaultValue();
}

Object value = property.getFlagValue(this.flagKey);
if (value == null) {
return this.getDefaultValue();
}

T convertedValue = this.deserializeValue(value);
if (convertedValue == null) {
return this.getDefaultValue();
}
return convertedValue;
}

public String getFlagDescriptionPage() {
return "flagDescription.jelly";
}

public String getFlagConfigPage() {
return "flagConfig.jelly";
}

@NonNull
@SuppressWarnings("rawtypes")
public static ExtensionList<UserExperimentalFlag> all() {
return ExtensionList.lookup(UserExperimentalFlag.class);
}

/**
* From the flag class, return the value of the flag for the current user
* If the returned value is {@code null},
* it means that either the class was not found or the current user is anonymous
*/
@SuppressWarnings("unchecked")
public static @CheckForNull <T> T getFlagValueForCurrentUser(String flagClassCanonicalName) {
Class<? extends UserExperimentalFlag<T>> flagClass;
try {
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(flagClassCanonicalName);
if (!UserExperimentalFlag.class.isAssignableFrom(clazz)) {
return null;
}
flagClass = (Class<? extends UserExperimentalFlag<T>>) clazz;
} catch (Exception e) {
return null;
}

UserExperimentalFlag<T> userExperimentalFlag = all().get(flagClass);
if (userExperimentalFlag == null) {
return null;
}

return userExperimentalFlag.getFlagValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* The MIT License
*
* Copyright (c) 2022, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.model.experimentalflags;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.UserPropertyDescriptor;
import java.util.HashMap;
import java.util.Map;
import net.sf.json.JSONObject;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;


/**
* Per user experimental flags to enable features that still not completely ready to be active by default.
*
* @since TODO
*/
public class UserExperimentalFlagsProperty extends UserProperty {
private Map<String, String> flags = new HashMap<>();

@DataBoundConstructor
public UserExperimentalFlagsProperty() {
}

public UserExperimentalFlagsProperty(Map<String, String> flags) {
this.flags = new HashMap<>(flags);
}

public @CheckForNull Object getFlagValue(String flagKey) {
return this.flags.get(flagKey);
}

@Extension(ordinal = -500)
@Symbol("experimentalFlags")
public static final class DescriptorImpl extends UserPropertyDescriptor {
@Override
public @NonNull String getDisplayName() {
return Messages.UserExperimentalFlagsProperty_DisplayName();
}

@Override
public @NonNull UserProperty newInstance(User user) {
return new UserExperimentalFlagsProperty();
}

@Override
public UserProperty newInstance(@Nullable StaplerRequest req, @NonNull JSONObject formData) throws FormException {
JSONObject flagsObj = formData.getJSONObject("flags");
Map<String, String> flags = new HashMap<>();
for (Object key : flagsObj.keySet()) {
String value = (String) flagsObj.get((String) key);
if (!value.isEmpty()) {
flags.put((String) key, value);
}
}
return new UserExperimentalFlagsProperty(flags);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
The MIT License
Copyright (c) 2022, CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<select class="jenkins-select__input" name="[${it.flagKey}]">
<f:option selected="${flagValue == null}" value="">
<j:if test="${it.getDefaultValue() == true}">${%Default_True}</j:if>
<j:if test="${it.getDefaultValue() == false}">${%Default_False}</j:if>
</f:option>
<f:option selected="${flagValue == true}" value="true">${%True}</f:option>
<f:option selected="${flagValue == false}" value="false">${%False}</f:option>
</select>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The MIT License
#
# Copyright (c) 2022, CloudBees, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Default_True=Default (enabled)
Default_False=Default (disabled)
True=Enabled
False=Disabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# The MIT License
#
# Copyright (c) 2022, CloudBees, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

UserExperimentalFlagsProperty.DisplayName=Experiments
Loading

0 comments on commit af09d67

Please sign in to comment.